How long does my endpoint have to respond?
15 seconds. That is the read timeout on the delivery call; a connection that is not established within 3 seconds fails earlier. Exceeding either counts as a failed delivery and the record goes back into the retry schedule. So do not do your business work inside the request. Verify the signature, persist the event, answer, and process asynchronously — the pattern the best-practices page recommends. A handler that calls three of your own services before answering is the most common reason a correctly-written integration shows up asFAILED.
I return HTTP 200. Why is the delivery still counted as failed?
Because the status code is not what decides it. The decision reads your response body, and a delivery counts as successful only when both of these hold:- The response is a 2xx — any 4xx or 5xx fails before the body is read.
- The body is a JSON object with a
messagefield whose value issuccess.
failed or unknown (both of which mean
“retry me”), an empty body, a different field name, an HTML error page, or a
body that cannot be parsed as JSON at all.
Three specifics that account for most of the surprises:
Does the response Content-Type have to be application/json?
In practice, yes — sendContent-Type: application/json. The response is read as
JSON, so a body served as text/plain or text/html cannot be parsed and the
delivery is recorded as failed even when the bytes themselves say
{"message":"success"}. The published
overview shows the header on the merchant
response for exactly this reason.
If signature verification fails, should I reject the request or process it anyway?
Never process it. Verify first, and only act on the event once verification passes — this is also an acceptance requirement: on a verification failure you must not update local state, and must read the real status from the inquiry API instead. What to return follows from that: answer{"message":"failed"}, which keeps
the record in the retry schedule, and go find out why. A signature that will not
verify is a configuration problem on one side or the other, and it will not fix
itself by being retried — so treat the retries as breathing room, not as a fix,
and reconcile through the inquiry API in the meantime.
If your signature verification fails only after you re-serialize the payload,
that is the cause: verify over the raw request body bytes, never over a
parsed-and-re-stringified copy. Field order, whitespace and number formatting all
change under a round trip, and the signature was computed over the original
bytes. See
Webhook signature verification.
What exactly does “8 retries” mean, and are the intervals guaranteed?
Eight attempts including the first, with growing gaps — the schedule is on Webhook retries, spanning roughly 20 hours end to end. Two qualifications the schedule alone does not tell you:- The intervals are minimums, not appointments. A retry becomes eligible at its scheduled time and is dispatched when the delivery worker next picks it up. Under a backlog an attempt can land later than the table suggests. Never build logic that assumes a retry arrives at an exact time.
- Once the budget is spent the record is finally failed and no further
automatic attempt is made. The Portal shows this as a
FAILEDrecord with no next retry time.
Why is “next retry time” sometimes empty?
Because it is only populated while an automatic retry is actually still scheduled. The field is filled for a record that failed and has retry budget left; it is empty for a delivered record and for a record whose retries are exhausted. That makes the pairing of the two columns the real reading, not the status alone:After all attempts fail, what do I do?
Read the object’s current state from its own inquiry API and reconcile — the authoritative state always lives on the object, not in the notification. The failure-recovery guidance lists the same three steps: query the object, reconcile in batch periodically, and contact support if deliveries never arrive at all. You can also replay from the Portal once your endpoint is fixed, in single, multi-select or by-date-range mode, up to 50 records per request. Note that a replay against a still-broken endpoint consumes an attempt from the same budget — so fix first, then replay. Details in Webhook Event Dictionary & Batch Replay.Why is Resend greyed out on a record?
There are exactly three reasons a record cannot be resent:
Note what is not on that list: a record that already delivered successfully
can be resent. That is deliberate — a delivery Waffo recorded as accepted may
still not have been processed on your side, so replaying it is allowed.
Resend also requires the webhook-resend permission on your Portal role. Without
it the action and the row selection are not rendered at all, rather than shown
disabled — so an operator who sees no resend control at all is looking at a
permissions question, not a record-eligibility question. Ask your Portal Admin.
Where does each event’s callback URL come from?
This is the single most common cause of “no notification arrived”: no URL was resolved, so nothing was ever sent and no record was written.
Two consequences worth designing around:
- There is no global payment notification URL. Payment notifications go only
to the
notifyUrlyou pass on order create, and that field is mandatory precisely because there is no fallback. You cannot configure one address in the Portal and stop sending the parameter. - Chargeback notifications have no request-level parameter. If the Portal
URL is empty, those events are simply not delivered and leave no log record. An
empty chargeback result on the webhook log page almost always means the
Chargeback Notify URL was never configured. The same applies to tokenization
notifications for cards tokenized during a payment; a standalone tokenization
request without a
notifyUrlis likewise not notified.
I changed my notification URL. Where do notifications already in the retry queue go?
To the old URL. The callback URL is stored on each notification record when that record is created, and every retry — and every manual resend — sends to the stored value. Editing the Portal configuration, or passing a differentnotifyUrl on a new order, affects only records created afterwards.
Plan a URL migration accordingly: keep the old endpoint accepting traffic until
the in-flight window has drained, rather than cutting it over the moment the new
URL is saved.
What does the notification URL field accept? Must it be HTTPS?
Validation, at save time, accepts:- A URL beginning with
http://orhttps:// - ASCII characters only, and no whitespace
- A parseable address with a host — ports, paths and query strings are all allowed
- Up to 512 characters for the Refund and Chargeback Notify URLs
- An empty value, which clears the configuration
http:// passes validation, but the published
configuration requirements state HTTPS
with TLS 1.2 or above, and a webhook carries payment data.
One hard constraint that is not obvious from the validation rules: your TLS
certificate must be trusted by a public certificate authority. The delivery
client uses the default trust store with no exception path, so a self-signed or
otherwise untrusted certificate fails the connection and every attempt fails. A
private CA is not usable here.
Can the same event arrive more than once? What should I dedupe on?
Yes, duplicates are possible, and your handler must be idempotent — the acceptance criteria test for exactly this. Sources of a repeat include an internal redelivery, a manual resend, and an authorization-stage notification followed by a final one on the same order. There is no event id or delivery id in the payload. The body is{"eventType": ..., "result": {...}}, and the only header of note is the
signature. So dedupe on eventType plus the business identifier inside
result — the payment order, refund order, subscription, payout order,
withdrawal order or token the event is about — combined with the status you are
acting on. Deduping on the identifier alone will make you drop the final status
of an order whose authorization you already recorded.
Is the arrival order guaranteed? Could I get “failed” before “succeeded”?
No, the order is not guaranteed, and yes — you can receive notifications out of order. Retries, queueing and network latency all reorder deliveries. The rule that makes this safe is on the best-practices page: do not drive your state machine from arrival order. Treat each notification as a trigger, and take the terminal state from the object — either from the payload’s status field with a guard against going backwards, or by calling the inquiry API after any notification. For subscriptions this is the documented pattern: after either a status or a period-changed notification, call subscription inquiry once and run a single unified flow on the result.On an auth-then-capture order, do I get a notification at the authorized stage?
Yes, and it arrives asPAYMENT_NOTIFICATION on the same order as the final
one — not as a distinct event type. You distinguish them by the order status in
the payload, not by the event:
This is also why deduping on the order id alone is wrong: two notifications for
one order are expected here, and only one of them is terminal. See
Auth and capture.
Will a notification ever carry a non-terminal status? Should I re-query?
Yes — as the table above shows, an order can be notified while still in progress. Act on terminal statuses; for a non-terminal one, record it and wait. If you need certainty at that moment, call the inquiry API rather than inferring. The one status that must never make you close an order yourself is the unknown result: onE0001, keep the order open, retry with identical parameters, and wait
for the inquiry result or the notification. Closing it yourself is a documented
acceptance failure — see
Integration acceptance criteria.
Do I get a notification when an order is cancelled, times out, or fails?
Yes — all three produce aPAYMENT_NOTIFICATION, and all three land on the same
terminal status: ORDER_CLOSE. There is no separate CANCELLED, EXPIRED
or FAILED status value.
To tell them apart, read the failure detail carried in the payload’s
orderFailedReason, which contains orderFailedCode and
orderFailedDescription. So the correct handling shape is: ORDER_CLOSE means
“do not fulfil”; orderFailedReason tells you what to say to the customer and
whether retrying makes sense.
Do I have to subscribe to events, or does everything just fire?
There is no subscription registry and no per-event opt-in. An event is delivered whenever a callback URL resolves for it under the table above — which means your control over what you receive is exactly your control over which URLs exist. The practical reading for a minimal integration: passnotifyUrl on order create
and handle payment notifications; leave the Portal-configured URLs empty for
event families you have not built handlers for, and pick those states up from the
inquiry APIs instead. Polling works for every object you can enumerate:
chargebacks through /api/v1/chargeback/list and /api/v1/chargeback/inquiry,
token state through /api/v1/tokenization/inquiry. The cost is latency — and for
chargebacks the evidence deadline runs from case creation, so if you handle
disputes at all, configure the Chargeback Notify URL rather than relying on
polling.
Can I find my payout and withdrawal notifications on the Portal webhook page?
Not through the page. The webhook log page searches a fixed set of seven event families — payment, refund, tokenization, chargeback and the three subscription events — and payout and withdrawal notifications are not among them, so they will not appear however you filter. For those, read state from the payout and withdrawal inquiry APIs. And note the matching configuration asymmetry: neither has a Portal-configured URL, so their notification address is whatever you passed on the request that created them.How do I know a notification never got delivered?
Today, by looking rather than by being told. There is no merchant-facing alert when deliveries fail, and no merchant API for querying notification records. Your two routes are:- The Portal webhook log, within its 90-day window — filter for failures, and use the next-retry column to separate “still retrying” from “finally failed”.
- Reconciliation against the inquiry APIs. Periodically query the objects you are expecting terminal states for and compare against your own records. This is the recommended safety net and the only one that also covers events the Portal page does not track.
Do you deliver from a fixed set of IP addresses I can allowlist?
There is no published egress IP range for webhook delivery, and no static egress address is configured — so there is nothing you can pin. Allowlisting Waffo’s callback source by IP is not something to design around. What to do instead: authenticate the callback rather than its origin. Every webhook carries a signature over the raw body, signed with the Waffo private key and verifiable with the Waffo public key from Merchant Portal → Integration. A verified signature is a stronger guarantee than a source-IP match, and it does not break when infrastructure changes.How do I receive callbacks on my laptop during development?
YournotifyUrl must be reachable from the public internet — localhost and
private addresses cannot be reached. Use a tunnelling tool and register the
tunnel’s public URL as your notifyUrl; the published
overview shows the commands for two common
ones.
Two practicalities: the tunnel hostname usually changes each time you restart it,
so pass the URL per order rather than saving it in Portal configuration, and the
certificate presented must be publicly trusted — which the hosted tunnels
provide and a local self-signed server does not.
Sandbox delivery follows this same contract, including the retry schedule — see Sandbox Simulation Limits FAQ for what the sandbox can and cannot make happen in the first place.