Skip to content
Merxian

Webhooks

Process events

Merxian delivers each event at least once and in no fixed order. Build your handler so that a duplicate or a late event does no harm.

On this page

What Merxian guarantees#

Guarantee Meaning for your handler
At least once An event can arrive more than once. A duplicate can arrive even after your endpoint returned 2xx.
Same event, same body Every delivery of an event has the same id and the same body.
No order Events can arrive in a different order from the changes. payment.succeeded can arrive before payment.processing.
Snapshot The payload shows the resource when Merxian created the event. It can be older than the current state.

Process each event once#

Store the event id when you process an event. Before you process an event, check whether you stored its id.

handler.js
export async function handleEvent(event, db) {
  // processed_events has a unique key on event_id.
  const inserted = await db.insertIfAbsent('processed_events', { event_id: event.id })
  if (!inserted) return // A duplicate. It was processed before.

  switch (event.type) {
    case 'payment.succeeded':
      await markPaid(event.data.object.transaction_id)
      break
    case 'refund.succeeded':
      await recordRefund(event.data.object.id, event.data.object.amount)
      break
    default:
      // Ignore event types that you do not use.
      break
  }
}

Put the check and the effect in one unit of work when you can. Then a crash between the two does not lose the event or process it twice.

Make each effect safe to repeat too. For example, “mark transaction txn_0F8mQ2rXbT4kL9pZa as paid” is safe to repeat. “Add one to the paid count” is not.

Do not depend on order#

Do not assume that events arrive in the order of the changes. Use one of these methods:

  • Read the current state. When an event arrives, read the resource from the API, for example with Retrieve a payment. Act on the state that the API returns.
  • Move only forward. Keep your own state for each record. Accept an event only if it moves the record forward. For example, after you mark a payment as succeeded, ignore a later payment.processing for that payment.

Final states are a safe signal. A transaction.completed event means that the transaction collected its full amount, in any order of arrival.

Answer before slow work#

Your endpoint must answer within 10 seconds. Use this order:

  1. Verify the signature. See Verify signatures.
  2. Store the event, for example in a queue or a table.
  3. Return 200.
  4. Process the event from the queue.

If processing fails, retry from your queue. Do not return a 5xx status to get a new delivery for a failure in your own processing. Merxian retries only for a limited time. See Delivery and retries.

Handle unknown events and fields#

  • Ignore an event type that you do not use. Return 2xx for it, so that Merxian does not retry it.
  • Ignore fields that you do not know. Merxian can add fields to a payload version.
  • Read state values as strings. Webhook state values can differ from API values. See State values in payloads.

Try refund, payment.succeeded,POST /v1/transactions, orIdempotency-Key.