Plaidly Docs

Webhook Delivery Status Runbook

Visibility, retry, and audit policy for outbound webhook deliveries

Webhook Delivery Status Runbook

Use this page for BDT-319 on the M6 Observability + Settlement Ops milestone.

The goal is to make failed merchant callbacks visible and recoverable without exposing secrets or creating an unbounded retry loop.

Delivery attempt record

One delivery_id groups all attempts for a single outbound webhook event. Each attempt should expose the same operational fields so support can tell whether a delivery is still moving, retrying, or terminal.

FieldMeaningMerchant-safe
delivery_idStable identifier for the outbound webhook eventYes
attempt_idUnique identifier for one send attemptYes
attempt_number1-based attempt count for the deliveryYes
statusqueued, sending, delivered, retry_scheduled, failed, or undeliverableYes
event_typeWebhook event name such as payment_session.completedYes
merchant_id / merchant_nameTarget merchant contextYes
webhook_urlDestination endpoint URL, without credentialsYes
response_codeExact HTTP status if the merchant replied, or a transport class such as timeout / dns_error / tls_errorYes, redacted to code or class only
latency_msTime from dispatch to response or failureYes
next_retry_atNext scheduled automatic retry timeYes
retry_countAutomatic attempts made so farYes
manual_retry_countManual retries made so farYes
last_error_classNormalized failure class for dashboards and supportYes
request_id / session_idCorrelation IDsYes
payload_hashDigest for correlation without storing the raw bodyYes
created_at / updated_at / last_attempt_atOperational timestampsYes

Keep the raw response body, response headers, delivery secret, signature material, and operator notes out of the merchant surface. Store them only in operator audit or incident records.

Status and retry meaning

Latest resultStatusNext retryNotes
2xx responsedeliveredNoneDelivery succeeded. Show the final latency only.
timeout / transport errorretry_scheduledCompute from backoffTreat as transient unless it repeats across several attempts.
5xx responseretry_scheduledCompute from backoffMerchant endpoint is unhealthy or overloaded.
429 responseretry_scheduledCompute from backoffMerchant is rate limiting the callback path.
4xx other than 429retry_scheduled or failed, depending on retry budgetUsually present until the retry budget is exhaustedUsually a payload, auth, or callback configuration problem.
Retry budget exhaustedundeliverableNoneDo not keep retrying automatically. Escalate to support and the merchant.

For the detail page and dashboard, show the last attempt's response_code, latency_ms, retry_count, and next_retry_at together. That combination tells operators whether the delivery is healthy, backing off, or stuck.

Bounded manual retry policy

Manual retry is a recovery action, not a new delivery primitive.

  1. Only an authorized operator can trigger a manual retry.
  2. Manual retry is allowed only for failed or undeliverable deliveries.
  3. The retry must reuse the original payload and webhook identity. It creates a new attempt record but does not mutate the original event.
  4. Cap manual retries at 3 per delivery_id within 24 hours.
  5. A fourth retry requires incident-owner approval and a written reason in the audit trail.
  6. Do not use manual retry while a healthy automatic retry is already scheduled or while the callback endpoint is still failing with the same class of error.
  7. Stop manual retries once the failure pattern points to a merchant-side fix that has not been applied.

Recommended manual retry reasons:

  • endpoint_fixed
  • secret_rotated
  • transient_outage
  • support_replay

Audit requirements

Every manual retry must produce an immutable audit event.

Record:

  • delivery_id
  • attempt_id
  • operator_id
  • operator_role
  • trigger_source such as dashboard, admin tool, or support workflow
  • reason
  • incident_id or ticket reference when available
  • before_status
  • after_status
  • manual_retry_count_before
  • manual_retry_count_after
  • response_code
  • latency_ms
  • created_at

Also retain the raw transport details needed for forensic review, but only in operator-only storage:

  • response body hash, not the raw body
  • response headers, redacted as needed
  • callback URL at the time of the attempt
  • request and trace identifiers
  • approval history when a retry exceeds the normal cap

Merchant-safe visibility

Merchant-facing views should explain what happened without exposing internal support actions.

Show:

  • delivery status
  • attempt count
  • response code or response class
  • latency
  • next retry ETA
  • a short failure class such as timeout, 5xx, auth, or configuration

Hide:

  • operator identity
  • internal notes
  • raw response bodies
  • response headers
  • webhook secrets
  • signature values
  • IP addresses and internal hostnames

If the merchant needs more detail, provide a support-safe summary that names the failure class and the next action, not the raw transport data.

Failure handling

Failure classDefault handlingManual retry guidance
timeout, dns_error, tls_error, connection resetKeep automatic retries with backoffReasonable after the merchant confirms the endpoint is reachable
5xxKeep automatic retries with backoffReasonable after the merchant says the service is healthy again
429Keep automatic retries with backoffOnly after the merchant removes the rate limit or increases capacity
401 / 403Treat as an auth or signature problemDo not keep retrying until the merchant updates the webhook secret or verification logic
404 / 410Treat as a stale callback URLStop retries and ask the merchant to update the endpoint
Persistent 4xx payload rejectionTreat as a contract or configuration problemFix the payload or merchant handler before retrying again

If the same delivery fails repeatedly with the same class of error, stop the retry loop, mark the delivery undeliverable, and surface the issue to support and the merchant.

On this page