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

# Webhook Event Dictionary & Batch Replay

> The seven webhook event types the Merchant Portal tracks, which identifier each one carries so you can match it back to your own order, how long the log is kept, and how to replay a window of failed deliveries with Retry by Date.

Three pages cover webhooks, and they answer different questions:

| Question | Page |
| - | - |
| What is the delivery contract, what must my endpoint return, how does automatic retry work? | [Webhook overview](/docs/en/developer-docs/webhook/overview) and [Webhook retries](/docs/en/developer-docs/webhook/retry) |
| What is in each event's payload? | [Webhook event type list](/docs/en/developer-docs/webhook/event-types) |
| How do I inspect one delivery in the Portal, why is a record missing, why is Resend greyed out? | [Webhook Event Log & Manual Resend](/docs/en/support/portal-webhook-log-faq) |

**This page is the dictionary and the bulk tool**: which events exist as Portal-trackable
events, which identifier each carries so you can match it to your own records, how far back the
log goes, and how to replay a whole window of failures at once.

## Which event types does the Portal track?

Seven. This is the exact set the **Event Type** filter offers, and the set searched when you
leave that filter empty:

| `eventType` | Label in the Portal | Fires when |
| - | - | - |
| `PAYMENT_NOTIFICATION` | Payment Notify | A payment completes or fails |
| `REFUND_NOTIFICATION` | Refund Notify | A refund completes or fails |
| `TOKENIZATION_NOTIFICATION` | Tokenization Notify | A token is generated, its status changes, or its card data is updated |
| `SUBSCRIPTION_STATUS_NOTIFICATION` | Subscription Status Notify | A subscription's own status changes — activation, cancellation, closure |
| `SUBSCRIPTION_PERIOD_CHANGED_NOTIFICATION` | Subscription Period Notify | A billing period reaches its terminal state |
| `SUBSCRIPTION_CHANGE_NOTIFICATION` | Subscription Change Notify | A subscription upgrade or downgrade completes |
| `CHARGEBACK_NOTIFICATION` | Chargeback Notify | A chargeback case is created or its status changes |

Two points that save wasted searching:

* **There is exactly one chargeback event.** Every chargeback creation and every status change
  on the case arrives as `CHARGEBACK_NOTIFICATION`. There is no separate alert event and no
  separate chargeback-refund event to subscribe to.
* **The value in the `eventType` column is the value above**, so what you read in the Portal is
  what you match on in your own code. The friendly label is display only.

## How do I match a webhook back to my own order?

Every record carries one **Order ID**, and which identifier that is depends on the event. This
is the crosswalk:

| Event | Order ID in the log | Your own matching key |
| - | - | - |
| `PAYMENT_NOTIFICATION` | `acquiringOrderId` — the Waffo payment order | Your `merchantOrderId`, carried in the payload |
| `REFUND_NOTIFICATION` | `acquiringRefundOrderId` — the Waffo refund order | Your refund's merchant order ID, plus the original order it refunds |
| `TOKENIZATION_NOTIFICATION` | `tokenId` | The token you stored against your customer |
| `SUBSCRIPTION_STATUS_NOTIFICATION` | The Waffo subscription ID | Your `merchantSubscriptionId`, carried in the payload |
| `SUBSCRIPTION_PERIOD_CHANGED_NOTIFICATION` | The Waffo subscription ID | Same, plus the period number in the payload |
| `SUBSCRIPTION_CHANGE_NOTIFICATION` | The **new** subscription ID produced by the change | The change request you submitted, plus the original subscription ID in the payload |
| `CHARGEBACK_NOTIFICATION` | An identifier of the chargeback notification itself | **`originalOrderId` inside the payload** — the disputed payment order |

The chargeback row is the one that surprises people, so state it plainly: **searching the log
by your original payment order number will not find a chargeback notification.** The Order ID
column there is the case's own identifier, not your order. Open the record and read
`originalOrderId` from the request body. The same caution applies in reverse — do not use the
chargeback log's Order ID as a key in your own system.

For subscriptions, note that an upgrade or downgrade produces a **new** subscription ID, so a
`SUBSCRIPTION_CHANGE_NOTIFICATION` will not match on the subscription ID you had before the
change. Read the original ID from the payload if you key on it.

## Which of these can I subscribe to, and where does the URL come from?

Only two callback URLs are yours to set in the Portal; the rest travel with the API call that
created the object. Nothing on the webhook log page changes a URL — that page is read-only by
design.

| Event | Where its URL comes from |
| - | - |
| `PAYMENT_NOTIFICATION` | `notifyUrl` on the order-create request |
| `REFUND_NOTIFICATION` | `refundNotifyUrl` on the refund request; if absent, the Refund Notify URL configured in the Portal |
| `TOKENIZATION_NOTIFICATION` | `notifyUrl` on the token-generate request; a Tokenization Notify URL is also configurable in the Portal |
| Subscription events | The `notifyUrl` held on the subscription itself |
| `CHARGEBACK_NOTIFICATION` | Only the Chargeback Notify URL configured in the Portal — it cannot be passed per request |

Configuring the Portal-side URLs is covered in
[Portal Self-Service Security Configuration](/docs/en/support/portal-security-config-faq). If an
event produced no log record at all, the usual cause is that no URL was resolved — see
[Webhook Event Log & Manual Resend](/docs/en/support/portal-webhook-log-faq).

## How far back does the webhook log go?

**90 days.** The page says so in a banner, the date picker will not let you select a day
earlier than 90 days ago, and Retry by Date is bounded by the same window. For anything older,
query the object itself through its inquiry API rather than looking for the notification.

Two related boundaries:

* The log starts at the point the feature went live. Older notifications are not backfilled, so
  an empty result for a very old transaction is expected rather than diagnostic.
* The list is sorted by **Submission Time**, newest first, by default.

## Can I replay a whole window of failures at once?

Yes — **Retry by Date**, which exists for exactly the case where your endpoint was down for a
period and you want the failures from that window re-driven without clicking them one by one.

| Input | Behaviour |
| - | - |
| Time range | Required. Bounded to the last 90 days; days outside that window cannot be selected. |
| Event types | Optional, multi-select. Leave it empty to cover all seven. |
| Estimate | The dialog counts the matching **failed** records as you change the inputs, and shows the number before you commit. |

Only failed records are eligible — successful and in-flight records are never included, so you
cannot accidentally re-deliver something your endpoint already accepted.

## How many can one batch resend?

**At most 50 records per request.** This applies to Retry by Date and to multi-select resend
alike, and it is enforced on both sides rather than being a UI hint.

What that means in practice:

| Situation | What happens |
| - | - |
| Estimate is 0 | The confirm button stays disabled — there is nothing to send |
| Estimate is 1–50 | Confirm proceeds |
| Estimate is above 50 | The dialog warns and confirm stays disabled: **narrow the time range or the event types**, then confirm |

The batch is **not** silently truncated to the first 50 — an over-limit range is refused
outright. That is deliberate: a truncated batch would leave you believing a window had been
replayed when most of it had not. For a large backlog, work through it in slices — by event
type first, since that usually splits the window along the lines your incident actually
followed.

## What do I get back after a batch resend?

A per-record result, summarised as counts rather than a single pass/fail:

| Outcome | What you see |
| - | - |
| All succeeded | *n record(s) resent successfully* |
| All failed | *Resend failed for all n record(s). Please try again later.* |
| Mixed | *n resent successfully, m failed.* |

"Succeeded" means your endpoint accepted the delivery on this attempt — the batch reports the
real delivery outcome, not merely that the request was queued. So a batch that comes back all
failed is telling you your endpoint is still not accepting the payload; fix it before replaying
again.

Records whose state changed between the estimate and the send — one that succeeded on an
automatic retry in the meantime, for instance — are counted in the failed tally rather than
resent, because they are no longer eligible.

## Does a manual or batch resend use up my automatic retries?

Yes — and this is worth knowing before you replay a window against an endpoint you have not
actually fixed.

A resend goes down the **same delivery path** as an automatic attempt. If your endpoint does
not accept it, the attempt is counted against the record's retry budget just like an automatic
one, and a next automatic retry is scheduled only while budget remains. Once the budget is
exhausted the record is finally failed: **Next Retry** shows `—` and no further automatic
attempt will be made.

The practical rule: **verify your endpoint first, then replay.** Replaying against a still-broken
endpoint burns attempts that were otherwise still scheduled to fire on their own.

## What does the Portal consider a "Failed" record?

`Failed` covers both records that have finally failed and records that failed this time but are
still in the automatic retry queue. The two are distinguished by the **Next Retry** column
rather than by the status:

| Status | Next Retry | Reading |
| - | - | - |
| Failed | A timestamp | Still scheduled to retry automatically — you can leave it alone |
| Failed | `—` | Retries exhausted; only a manual resend will move it |
| Successful | `—` | Delivered and accepted |

This matters for Retry by Date: the estimate counts **both** kinds of failed record, so a window
you replay may include records that would have recovered on their own.

## Why does a successful record show no response from my endpoint?

Because the Portal derives the display for a delivered record rather than storing what your
server sent. A successful record shows a `200` HTTP Response and a `success` body because that
is what "successful" means — a delivery only counts as successful when your endpoint returns
HTTP 200 **and** a body of `{"message":"success"}`, per
[Webhook overview](/docs/en/developer-docs/webhook/overview).

Real captured detail exists only on **failed** records, where the failure summary your endpoint
returned — or the transport error — is shown. So when you are debugging, the failed records are
the informative ones; there is nothing extra hiding behind the successful ones.

## Will resending create a second row in the log?

No. There is **one record per event per business object** — the same payment order's payment
notification is one row for its whole life. A resend updates that row's status, response and
attempt count in place; it does not add a row. The **Notify Count** in the detail view is your
attempt history.

That is also why you cannot use row counts to measure delivery volume, and why a resent record
loses its previous failure summary once a later attempt overwrites it.

***

If a record you expect is missing entirely rather than failed, start from
[Webhook Event Log & Manual Resend](/docs/en/support/portal-webhook-log-faq) — a missing record
almost always means no callback URL was resolved, which resending cannot fix.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.