Route / developers

Route webhooks

Availability: outbound delivery requires Route to approve your receiver’s HTTPS origin. Creating an endpoint alone does not enable delivery. Use payment polling until your receiver is enabled.

Webhooks report Route payment observations asynchronously. Verify and deduplicate each event, then read the current payment before updating your application. Events may be duplicated or arrive out of order.

Configure an endpoint#

Host the receiver on your application’s backend, for example https://your-app-domain.com/api/webhooks/route. A separate webhook service is not required. Verify the signature, durably accept the event, and return a 2xx response within ten seconds. Process longer work asynchronously.

Use Webhooks in the dashboard, or a server-side Route key with webhooks:write:

POST /v1/webhooks
Authorization: Bearer <Route key>
Content-Type: application/json
Idempotency-Key: <UUID>

{"name":"Payments backend","url":"https://your-app-domain.com/api/webhooks/route"}

The response contains the endpoint metadata and a whsec_… signing secret, shown once. Store it in your backend secret manager. It cannot be retrieved. A repeated creation key returns SECRET_ALREADY_ISSUED, preventing a second issuance. Creation or rotation without a UUID key returns 400 IDEMPOTENCY_REQUIRED and issues nothing. Rotate if the original response was lost.

GET /v1/webhooks requires webhooks:read and returns safe endpoint metadata and the latest 100 delivery records. No secret or encrypted secret is returned. Each endpoint and delivery record includes deliveryStatus: ready, disabled, or awaiting-activation. The response-level deliveryEnabled describes whether any receiver origin is enabled on Route; check your endpoint’s own status before relying on delivery.

When Route processes an event for delivery, it queues it only for currently enabled endpoints with an approved origin and matching scope. Events already processed while an endpoint was disabled or unapproved are not replayed after activation. A recent event still awaiting processing may be queued after activation if the endpoint already existed when that event was created. Deliveries expire 24 hours after the event was created, including older queued events. Poll payments during onboarding and read current payment state after activation; webhook activation is not a complete history replay.

  • PUT /v1/webhooks/{id} with {enabled,version} enables or disables delivery.
  • POST /v1/webhooks/{id}/rotate with {version} and a fresh UUID idempotency key issues a replacement signing secret.
  • POST /v1/webhooks/{id}/test with {} queues a clearly identified test event for an active, approved endpoint; otherwise it returns WEBHOOK_NOT_ACTIVE.
  • POST /v1/webhooks/{id}/retry with {deliveryId} restarts a dead or disabled delivery less than 24 hours old for an active, approved endpoint. The original event ID is retained. Expired deliveries cannot be replayed; read the current payment instead.

Endpoints created through an API key inherit its project and policy. The key can list and manage only endpoints for that policy, and those endpoints receive only payments belonging to it. Dashboard-created endpoints have project-wide scope. API keys, including keys bound to the default policy, do not list or manage project-wide endpoints; use the dashboard for them, or create the endpoint with the key that should manage it. Endpoint metadata exposes policy_id (null means project-wide); its scope cannot be changed after creation.

Endpoints created before mainnet 1.43.0-preview had no recorded scope. The upgrade attributed each one from its creation evidence and kept its enabled state and version where the creator could be established. If no API key of its creator could have created it, it stays project-wide. If only API keys of one policy could have created it, and no recent dashboard sign-in could have, it takes that policy, so that policy's keys can list and manage it again. If both were possible, or keys of several policies could have created it, the endpoint was disabled and shows scope_review_required: true. A dashboard user with webhook-management permission must review it and re-enable it as project-wide before it receives events. API keys cannot see or reactivate it; to manage that receiver through the API, create a replacement endpoint with the key. Each policy assignment (webhook.policy_scope_assigned) and review requirement (webhook.scope_review_required) is recorded in the workspace audit log as a system action.

Key revocation, member removal and role changes are checked on each request. Dashboard secret creation/rotation requires a sign-in less than 15 minutes old. A secret rotation is checked immediately before sending; a request already in flight may use the previous secret.

Verify the exact request body#

Headers:

  • X-Route-Event-Id: stable event UUID; use this for durable deduplication.
  • X-Route-Timestamp: Unix seconds for this delivery attempt.
  • X-Route-Signature: v1= followed by lowercase hexadecimal HMAC-SHA256.

Signing input is the UTF-8 string <eventId>.<timestamp>.<rawBody>. The HMAC key is the complete signing secret as UTF-8, including whsec_. Do not parse and reserialize JSON before verification. Reject timestamps more than five minutes in the past or future. Verify the signature, ensure the body id equals the header event ID, and atomically deduplicate the event with your business update.

Example receiver verification (Node.js, before JSON middleware):

import { createHmac, timingSafeEqual } from 'node:crypto';

export function verifyRouteEvent({secret, rawBody, headers}) {
  const id = headers['x-route-event-id'];
  const rawTime = headers['x-route-timestamp'];
  const signature = headers['x-route-signature'];
  if (typeof id !== 'string' || !/^\d{1,12}$/.test(rawTime ?? '') ||
      !/^v1=[0-9a-f]{64}$/.test(signature ?? '')) throw Error('Invalid signature');
  const time = Number(rawTime);
  if (Math.abs(Date.now() / 1000 - time) > 300) throw Error('Expired signature');
  const expected = createHmac('sha256', secret)
    .update(`${id}.${rawTime}.`).update(rawBody).digest();
  if (!timingSafeEqual(expected, Buffer.from(signature.slice(3), 'hex')))
    throw Error('Invalid signature');
  const event = JSON.parse(rawBody.toString('utf8'));
  if (event.id !== id) throw Error('Event identity mismatch');
  return event; // Persist/deduplicate event.id before acknowledging.
}

Events and delivery semantics#

Events include payment.created, payment.cancelled, payment.preparing, payment.awaiting_action, payment.submitted, payment.source_confirmed, payment.requires_attention, payment.completed, payment.refunded, payment.failed and webhook.test. For transactions sent by Route, payment.submitted follows the acknowledged broadcast; saving a signature does not emit it. An uncertain send emits payment.requires_attention, including an uncertain send of a spending approval; that approval stays the reason for attention until a later action is saved, even after tracking proves it was sent and next-action offers the next transaction. A wallet handoff (POST /transaction/broadcast) emits no event while Route waits for the wallet's hash: the payment reads WALLET_SUBMISSION_UNKNOWN. Once Route verifies a funding transaction's broadcast, payment.submitted follows. An externally registered source hash can also produce payment.submitted; that event is not proof of funding. So can a Stellar transaction the wallet broadcast itself, once Route finds and adopts its exact envelope. A proof-gated closure of an expired Stellar payment emits payment.cancelled and no later payment.requires_attention. A Stellar renewal or close check that finds the saved transaction's wallet sequence used, without a receipt Route can adopt, emits payment.requires_attention; the payment stays under review. Completed/refunded events require verified source and settlement receipts. Existing records created before eventing was installed are not backfilled.

A failed destination transaction, a failed or expired managed destination relay, or a managed relay whose submission response was lost (submission-unknown) emits payment.requires_attention even after source confirmation. When Route's single guarded automatic re-mint then resubmits the managed mint, the payment leaves review and emits the same event as any other recovery from review (payment.source_confirmed for a direct payment) until delivery is verified. A duplicate relay that failed only because the message was already minted is not reported as a destination failure: the payment completes from the recorded relay that minted it, or stays under review until that mint's hash is supplied. A temporary receipt, provider-status or destination-lookup outage emits nothing: tracking retries, a pending payment reads VERIFICATION_PENDING (an uncertain send or wallet handoff keeps its state), and the next event follows the next verified change. Any other tracking error, such as a receipt that does not prove the saved action or a removed provider connection, emits payment.requires_attention; a later temporary outage does not replace that error or emit an event, and it clears only when an observation succeeds. A detected wallet change to a mainnet approval, funding transaction or mint also emits this event, with the saved actionId; the action may still be ready because Route has not accepted the changed envelope. Read the payment and its next action before continuing. These observations do not replace an already verified completion or refund, and never authorize sending replacement funds.

Two-step routes also emit parent payment.intermediate_received and payment.second_leg_submitted events. Parent payloads identify paymentId, legPaymentId and leg; three-step routes additionally emit payment.second_intermediate_received and payment.third_leg_submitted. Parent completion requires verified settlement of every expected transfer. A parent stream combines its transfers: each transfer's parent event is emitted only when that transfer's observed state changes, so refreshing an earlier, settled transfer never repeats its milestone, and no parent event follows payment.completed. A parent payment.requires_attention is therefore not repeated for the same transfer until that transfer reaches another parent milestone: after an uncertain approval send, for example, a later uncertain send in that transfer appears only on the transfer's own stream, and the latest parent event still carries the approval's actionId. Read the payment or the transfer for its current action. A proof-gated cancellation of the first transfer ends the parent stream at payment.cancelled; the transfer's own stream still reports payment.requires_attention before its payment.cancelled. See composed payments.

Testnet payments emit payment.created and then one event per saved testnet status change, named after that status: payment.awaiting_action, payment.approval_confirmed, payment.source_pending, payment.attesting, payment.awaiting_destination, payment.destination_pending, payment.completed, payment.source_reverted, payment.destination_failed and payment.cancelled. A renewed customer-signed testnet mint emits payment.mint_renewed with authorizationId and renewedFrom, and no status. These events carry data.network: "testnet", except a payment.cancelled recorded by a proof-gated wallet-broadcast close, which carries cancellationHash instead.

The JSON envelope includes id, type, version, createdAt, projectId, and a small data object with paymentId, the observed status and the same version. data.status describes the event, not the payment's current paymentState. It uses its own vocabulary: never compare it with paymentState, and fetch the payment for its current state. Submission events also carry actionId and the saved action's submissionState; composed parent events carry legPaymentId, leg and intermediateWallet.

Event Mainnet data.status Testnet data.status paymentState usually read afterwards
payment.created DRAFT draft DRAFT
payment.preparing PREPARING — PREPARING
payment.awaiting_action, payment.approval_confirmed AWAITING_ACTION awaiting_action, approval_confirmed AWAITING_ACTION, or WALLET_SUBMISSION_UNKNOWN after a wallet handoff
payment.submitted, payment.source_pending SUBMITTED source_pending SOURCE_PENDING
payment.source_confirmed, payment.attesting, payment.awaiting_destination, payment.destination_pending SOURCE_CONFIRMED attesting, awaiting_destination, destination_pending DESTINATION_PENDING, SOURCE_PENDING until the source is final, or DELIVERED_PENDING_FINALITY
payment.intermediate_received, payment.second_intermediate_received (parent) INTERMEDIATE_RECEIVED, SECOND_INTERMEDIATE_RECEIVED — INTERMEDIATE_HELD
payment.second_leg_submitted, payment.third_leg_submitted (parent) SECOND_LEG_SUBMITTED, THIRD_LEG_SUBMITTED — The active transfer's pending state, such as SOURCE_PENDING
payment.requires_attention, payment.destination_failed REQUIRES_ATTENTION destination_failed REQUIRES_ATTENTION
payment.completed COMPLETED completed COMPLETED; testnet reads VERIFICATION_PENDING or DELIVERED_PENDING_FINALITY until every receipt is final
payment.refunded REFUNDED — REFUNDED
payment.failed, payment.source_reverted FAILED source_reverted FAILED
payment.cancelled CANCELLED cancelled CANCELLED

Each payment has its own strictly increasing webhook sequence, independent of payment/preparation revision numbers. Composed parent and child payments have separate sequences; testnet payments use the same sequencing contract. Repeated observations of the current event type are suppressed, but returning to an earlier state emits a new event with a higher version. Each payment.mint_renewed announces a different authorization, so consecutive renewals are each sent. A standalone webhook.test event has version 1. Fetch the current payment through Route to inspect evidence. Delivery is at least once and ordering is not guaranteed. Never regress your state based on a delayed event; compare versions and fetch the authoritative payment. Store event IDs durably.

HTTP 2xx acknowledges delivery. Timeouts, network errors, redirects and non-2xx responses retry with exponential backoff, up to eight attempts. For HTTP 429 and 503, a valid Retry-After can extend the delay up to one day. Disabled receivers are not contacted; re-enable the endpoint and explicitly retry its disabled deliveries when ready. Response bodies are discarded. Delivery records and safe attempt details are retained for 90 days. Expiry and retention do not remove payment history or accounting journals, and event sequences never restart. Dead deliveries less than 24 hours old can be explicitly retried without starting a new payment.

Current deployment boundary#

Delivery requires an approved receiver HTTPS origin. Only public DNS-form HTTPS URLs on the default port are accepted; credentials, fragments, literal IPs and reserved local names are rejected. Redirects are not followed. Contact Route to enable your receiver before relying on event delivery, and keep payment polling available while onboarding is pending.