Webhooks

Treat iframe confirmPayment / confirmSetup results as UX. Fulfill orders, mark a payment method saved, or trigger payouts only after a webhook (or a server-side retrieve).

Configure

In the dashboard, create a Webhook Endpoint (POST /webhook_endpoints in the reference). Use a different URL per environment. Amos signs deliveries — verify the signature before mutating state.

List and inspect deliveries with WebhookRequests and WebhookAttempts. You can resend a request from the API.

Events to handle first

Exact WebhookEventType values (do not invent names):

Event When
payment_intent.succeeded Authorized / sold. For capture_method: automatic_async, also watch charge.succeeded / charge.requires_capture and retrieve if you need capture state.
payment_intent.cancelled Abandoned or cancelled.
payment_intent.errored_authorization / errored_capture / errored_sale Processor error.
payment_intent.requires_payment_method / requires_confirmation / requires_capture / requires_review More customer or merchant action.
setup_intent.succeeded Payment method vaulted.
setup_intent.failed / errored / cancelled Save failed.

Full enum is WebhookEventType in the reference. Subscribe only to what you handle.

Handler rules

  1. Verify the signature.
  2. Return 2xx quickly. Do slow work asynchronously.
  3. Idempotent on event id — Amos retries.
  4. Do not trust the client. The browser can lie; the webhook cannot (if you verify).

The POST body is WebhookEventPayload:

{
  "id": "<stable event uuid — identical across retries>",
  "webhook_id": "<endpoint uuid>",
  "event": "payment_intent.succeeded",
  "data": {
    "payment_intent": { "id": "<uuid>", "state": "succeeded", "amount": 5000 }
  }
  "metadata": null
}

data is event-specific (OpenAPI additional properties). The payment_intent object above is an example, not a frozen schema — retrieve GET /payment_intents/{id} when a field is missing. Branch on event, not type. Idempotency key is id.

Signing: WebhookEndpoint includes a secret. The public OpenAPI does not document the signature header or algorithm. Do not invent HMAC. Verify using the scheme your dashboard / onboarding packet specifies; keep the raw body for that check before JSON.parse. Until that scheme is in the public spec, still require HTTPS, rotate secret if leaked, and fulfill only from events you can authenticate.

app.post("/webhooks/amos", express.raw({ type: "application/json" }), (req, res) => {
  // authenticate(rawBody, headers, endpoint.secret) — use the documented scheme
  const payload = JSON.parse(req.body.toString("utf8"));
  switch (payload.event) {
    case "payment_intent.succeeded":
      // fulfill using payload.data.payment_intent
      break;
    case "setup_intent.succeeded":
      // save payment method from payload.data.setup_intent
      break;
    default:
      break;
  }
  res.status(200).end();
});

When there is no webhook yet

GET /payment_intents/{id} and GET /setup_intents/{id} after confirm. Use intent state. Do not read last_payment_error — it was removed from PaymentIntent.