> ## Documentation Index
> Fetch the complete documentation index at: https://docs.moduluslabs.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Events and Payloads

> Understand Payment Webhook event bodies, resource identifiers, and timestamps

All Modulus-defined fields use `snake_case`. Merchant-owned keys inside
`metadata` preserve their original spelling and casing. These examples show
the complete canonical envelope delivered to your endpoint.

<Tabs>
  <Tab title="Succeeded">
    ```json theme={null}
    {
      "id": "609b0999-b5dd-472e-98a0-d651cbbdffd4",
      "type": "payment.succeeded",
      "created_at": "2026-09-18T09:16:21.976Z",
      "data": {
        "id": "01a0b3cd-7069-7d82-9497-90dfa88b2822",
        "payment_attempt_id": "01a0b3cd-7062-7bdb-b2f4-8cb37ce3f231",
        "payment_intent_id": "b7e2c1a4-9f3d-4c6b-8a21-5e0f7d9c3b18",
        "order_reference": null,
        "status": "CAPTURED",
        "amount": 1000,
        "currency": "PHP",
        "payment_method": "ECOM",
        "card_brand": "VISA",
        "card_last_four": "1005",
        "correlation_id": "01a0b3cd-7062-7bdb-b2f4-8cb37ce3f231",
        "idempotency_key": "checkout-2026-0001",
        "created_at": "2026-09-18T09:16:20.460Z",
        "updated_at": "2026-09-18T09:16:21.951Z",
        "amount_details": {
          "amount_authorized": 1000,
          "amount_capturable": 0,
          "amount_received": 1000,
          "amount_refunded": 0
        },
        "receipt": {
          "approval_code": "407506",
          "reference_number": "01a0b3cd-7069-7d81-ada1-d602fbc5aaaa",
          "batch_number": null,
          "invoice_number": "623956",
          "network_transaction_id": "016153570198200",
          "retrieval_reference_number": "626109623956",
          "reconciliation_id": "f5f831ed7b4d4521885106",
          "response_code": "00"
        },
        "merchant_details": {
          "merchant_id": "638a13b2-b1f2-4cd2-acbc-dcff4d5ecd95",
          "partner_name": "DEV Test Ecom",
          "merchant_name": "Ecom Test Aqua Mart",
          "branch_id": "c98893ca-15e6-42f8-a1c8-2162c788e0d5",
          "branch_name": "Test Ecom Aquamart - Makati Branch"
        },
        "activation_code": null,
        "decline_details": null,
        "failure_details": null,
        "metadata": {
          "merchantOrderId": "ORDER-2026-0001",
          "channel": "web"
        }
      }
    }
    ```
  </Tab>

  <Tab title="Declined">
    ```json theme={null}
    {
      "id": "75e0567c-1298-4c68-a088-22ae7417a493",
      "type": "payment.declined",
      "created_at": "2026-09-18T09:23:23.197Z",
      "data": {
        "id": "01a0b3d3-c396-7d84-8d85-c9e929ddec8f",
        "payment_attempt_id": "01a0b3d3-c390-7b3e-9f70-d2d46a48797b",
        "payment_link_id": "76c3f848-33ce-4ea8-947e-ed27d57e3926",
        "order_reference": "ORDER-2026-0002",
        "status": "DECLINED",
        "amount": 405100,
        "currency": "PHP",
        "payment_method": "ECOM",
        "card_brand": "MASTERCARD",
        "card_last_four": "2151",
        "correlation_id": "01a0b3d3-c390-7b3e-9f70-d2d46a48797b",
        "idempotency_key": "ORDER-2026-0002",
        "created_at": "2026-09-18T09:23:14.966Z",
        "updated_at": "2026-09-18T09:23:23.159Z",
        "amount_details": {
          "amount_authorized": 0,
          "amount_capturable": 0,
          "amount_received": 0,
          "amount_refunded": 0
        },
        "receipt": {
          "approval_code": null,
          "reference_number": "01a0b3d3-c396-7d81-ad18-0b02ba19fe94",
          "batch_number": null,
          "invoice_number": "414177",
          "network_transaction_id": "0918MCC438308",
          "retrieval_reference_number": "626109414177",
          "reconciliation_id": "d37d909fee4746649981a7",
          "response_code": "51"
        },
        "merchant_details": {
          "merchant_id": "638a13b2-b1f2-4cd2-acbc-dcff4d5ecd95",
          "partner_name": "DEV Test Ecom",
          "merchant_name": "Ecom Test Aqua Mart",
          "branch_id": "c98893ca-15e6-42f8-a1c8-2162c788e0d5",
          "branch_name": "Test Ecom Aquamart - Makati Branch"
        },
        "activation_code": null,
        "decline_details": {
          "error_code": "INSUFFICIENT_FUNDS",
          "error_message": "The card has insufficient funds to complete this transaction."
        },
        "failure_details": null,
        "metadata": {
          "merchantOrderId": "ORDER-2026-0002"
        }
      }
    }
    ```
  </Tab>

  <Tab title="Failed">
    ```json theme={null}
    {
      "id": "b4c87ca6-8a06-42c8-ad9b-2c056ff52689",
      "type": "payment.failed",
      "created_at": "2026-09-21T09:26:04.922Z",
      "data": {
        "id": "01a0c349-6cf8-7462-b621-18bb52a226fc",
        "payment_attempt_id": "01a0c349-6cf7-7645-bf07-819981195afc",
        "payment_intent_id": "1dcafdaa-516f-40db-80ba-09b7ca968531",
        "order_reference": null,
        "status": "FAILED",
        "amount": 1000,
        "currency": "PHP",
        "payment_method": "ECOM",
        "card_brand": "VISA",
        "card_last_four": "1039",
        "correlation_id": "01a0c349-6cf7-7645-bf07-819981195afc",
        "idempotency_key": "01a0c349-6cf7-7645-bf07-819981195afc",
        "created_at": "2026-09-21T09:26:04.280Z",
        "updated_at": "2026-09-21T09:26:04.863Z",
        "amount_details": {
          "amount_authorized": 0,
          "amount_capturable": 0,
          "amount_received": 0,
          "amount_refunded": 0
        },
        "receipt": {
          "approval_code": null,
          "reference_number": "01a0c349-6cf8-7461-9697-b27399573ce4",
          "batch_number": null,
          "invoice_number": null,
          "network_transaction_id": null,
          "retrieval_reference_number": null,
          "reconciliation_id": "be704de7d41a4ef09ae2b2",
          "response_code": null
        },
        "merchant_details": {
          "merchant_id": "638a13b2-b1f2-4cd2-acbc-dcff4d5ecd95",
          "partner_name": "DEV Test Ecom",
          "merchant_name": "Ecom Test Aqua Mart",
          "branch_id": "c98893ca-15e6-42f8-a1c8-2162c788e0d5",
          "branch_name": "Test Ecom Aquamart - Makati Branch"
        },
        "activation_code": null,
        "decline_details": null,
        "failure_details": {
          "error_code": "INTERNAL_ERROR",
          "error_message": "An internal error occurred while processing the payment."
        },
        "metadata": null
      }
    }
    ```
  </Tab>

  <Tab title="Expired">
    ```json theme={null}
    {
      "id": "72ca176c-393f-4bc3-914a-68b040d77384",
      "type": "payment.expired",
      "created_at": "2026-09-18T10:02:05.208Z",
      "data": {
        "id": null,
        "payment_attempt_id": null,
        "payment_link_id": "415d81d1-1aad-4ee3-8756-1dc3ad8aff9d",
        "order_reference": "ORDER-2026-0004",
        "status": "EXPIRED",
        "amount": 1000,
        "currency": "PHP",
        "payment_method": "ECOM",
        "card_brand": null,
        "card_last_four": null,
        "correlation_id": "01a0b3f8-95ac-74da-8ad1-63ea3776b1cb",
        "idempotency_key": "ORDER-2026-0004",
        "created_at": null,
        "updated_at": null,
        "amount_details": {
          "amount_authorized": 0,
          "amount_capturable": 0,
          "amount_received": 0,
          "amount_refunded": 0
        },
        "receipt": null,
        "merchant_details": {
          "merchant_id": "638a13b2-b1f2-4cd2-acbc-dcff4d5ecd95",
          "partner_name": "DEV Test Ecom",
          "merchant_name": "Ecom Test Aqua Mart",
          "branch_id": "c98893ca-15e6-42f8-a1c8-2162c788e0d5",
          "branch_name": "Test Ecom Aquamart - Makati Branch"
        },
        "activation_code": null,
        "decline_details": null,
        "failure_details": null,
        "metadata": {
          "merchantOrderId": "ORDER-2026-0004"
        }
      }
    }
    ```
  </Tab>
</Tabs>

## Nullable fields

| Field                                                                      | When it is `null`                                                                                                    |
| -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `data.order_reference`                                                     | The originating Payment Link or Payment Intent did not supply one.                                                   |
| `data.card_brand`, `data.card_last_four`                                   | Card details were unavailable, or the event has no payment attempt.                                                  |
| `data.receipt`                                                             | No transaction exists, including `payment.expired`. For transaction events, individual receipt fields can be `null`. |
| `data.activation_code`                                                     | Always `null` for current Ecom events.                                                                               |
| `data.decline_details`                                                     | Every event except `payment.declined`.                                                                               |
| `data.failure_details`                                                     | Every event except `payment.failed`.                                                                                 |
| `data.metadata`                                                            | The originating resource had no metadata, or optional legacy metadata could not be decoded safely.                   |
| `data.id`, `data.payment_attempt_id`, `data.created_at`, `data.updated_at` | `payment.expired`, because no transaction or attempt exists.                                                         |

For a declined event, `decline_details` is populated and `failure_details` is
`null`. For a failed event, the inverse is true. Processor-specific error text
is not forwarded; `error_code` and `error_message` use the documented
merchant-safe values.

### Decline error codes

These appear in `decline_details.error_code` on `payment.declined` events.
Branch on `error_code`, not `error_message`.

| Code                 | Meaning                                                             |
| -------------------- | ------------------------------------------------------------------- |
| `INSUFFICIENT_FUNDS` | The card has insufficient funds.                                    |
| `DO_NOT_HONOR`       | The issuer declined without a specific reason.                      |
| `LOST_OR_STOLEN`     | The card is reported lost or stolen.                                |
| `CARD_EXPIRED`       | The card has expired.                                               |
| `CARD_BLOCKED`       | The card or account is blocked or suspended.                        |
| `INVALID_CARD`       | The card number or account is invalid.                              |
| `RESTRICTED_CARD`    | The card is restricted or not authorized for this transaction type. |
| `DECLINED`           | A general decline that does not match a more specific category.     |

### Failure error codes

These appear in `failure_details.error_code` on `payment.failed` events.

| Code                          | Meaning                                                         |
| ----------------------------- | --------------------------------------------------------------- |
| `ACQUIRER_ROUTER_UNAVAILABLE` | The payment processor could not be reached.                     |
| `GATEWAY_TIMEOUT`             | The payment processor did not respond within the expected time. |
| `INTERNAL_ERROR`              | An internal error occurred while processing the payment.        |

Metadata is the snapshot belonging to the originating Payment Link or Payment
Intent. The two sources are never merged, and merchant-owned metadata keys are
not converted to `snake_case`.

## Resource identifiers

* Payment Link events contain `data.payment_link_id` and omit `data.payment_intent_id`.
* Payment Intent events contain `data.payment_intent_id` and omit `data.payment_link_id`.
* The two identifiers are never merged or used as aliases.

## Timestamps

* Envelope `created_at`: when the webhook event was created.
* `data.created_at`: when the underlying transaction was created.
* `data.updated_at`: when that transaction was last updated.

For `payment.expired`, no transaction or payment attempt exists. The transaction
fields `data.id`, `data.payment_attempt_id`, `data.created_at`, and
`data.updated_at` are therefore `null`. Use the envelope `created_at` for the
time the expiry was observed and the event was created; expiry detection is
lazy, so this can be later than the resource's configured `expires_at`.

All non-null timestamps are UTC RFC 3339 values. Delivery order is not
guaranteed; use `data.updated_at` when comparing transaction state and
deduplicate on the envelope `id`.

## Event inspection

Use `GET /v1/webhook_events` and `GET /v1/webhook_events/{id}` to inspect the
persisted canonical event. Use the reporting API as the authoritative payment
state when reconciling a delayed or missing webhook.

The event-management response uses these fields:

| Field        | Description                                                                                          |
| ------------ | ---------------------------------------------------------------------------------------------------- |
| `id`         | Unique webhook event identifier used for retrieval and retriggering.                                 |
| `type`       | Payment event type.                                                                                  |
| `status`     | Receiver submission status. `SUBMITTED` means accepted for delivery, not confirmed merchant receipt. |
| `created_at` | UTC RFC 3339 timestamp when the webhook event was created.                                           |
| `data`       | Persisted canonical event data returned by the detail operation.                                     |

List operations return `data` and `pagination`. Pass `pagination.next_cursor`
as the next request's `cursor`; `next_cursor` is `null` when `has_more` is
`false`.
