Skip to main content
Paycrest supports two payment directions: off-ramp (stablecoin → fiat) and on-ramp (fiat → stablecoin). Both flows follow a similar lifecycle through the protocol.

Off-ramp Lifecycle (Stablecoin → Fiat)

1

initiated

Sender creates a payment order via the API or Gateway contract. The order is recorded and a receive address (deposit address) is returned. Funds have not yet arrived.
2

deposited

Stablecoins are detected at the receive address. The protocol confirms the deposit and begins matching.
3

pending

The aggregator has assigned the order to a suitable liquidity provider. The provider’s provision node is processing the fiat payout.
4

fulfilling

The provision node is actively disbursing fiat to the recipient’s bank account or mobile wallet via a local PSP.
5

validated

The provider has confirmed successful fiat delivery. The order is waiting for onchain settlement.
6

settling

Onchain settlement is in progress — the escrowed stablecoins are being released to the provider.
7

settled

The order is fully closed. Stablecoins have been released to the provider; fiat has been delivered to the recipient.
Performance: The majority of orders complete in under 30 seconds from deposit to completion. If fulfillment fails after a deposit is received, the sender is automatically refunded.
expired vs refunded: These are two distinct failure modes. expired means the receive address (off-ramp) or virtual account (on-ramp) was never funded — no deposit was received. refunded means a deposit was received but the order could not be fulfilled (e.g. no available provider, PSP unavailability, rate constraints) — and the deposited funds have been returned to the sender.

On-ramp Lifecycle (Fiat → Stablecoin)

1

initiated

Sender creates an on-ramp order via the API. The protocol returns provider account details (virtual bank account or mobile wallet) for the user to deposit fiat into.
2

pending

The aggregator has matched the order to a provider. Waiting for the user’s fiat deposit to be confirmed by the provider.
3

fulfilling

Provider has confirmed fiat receipt. The protocol is preparing to release stablecoins to the recipient’s wallet address.
4

validated

Fiat receipt confirmed and stablecoin release authorized.
5

settling

Onchain transfer of stablecoins to the recipient’s wallet is in progress.
6

settled

Stablecoins have been delivered to the recipient’s wallet. The order is fully closed.
expired means the virtual account was never funded — no fiat deposit was received. refunded means fiat was received but the order could not be completed, and the deposited funds have been returned.

Order Statuses

The complete set of order status values:

Webhook Events

Paycrest sends webhooks to your configured endpoint as an order progresses. Most events are named after the status the order just entered (payment_order.<status>); a small number report an event that is not a status change, and are marked below. Failed deliveries are retried automatically with exponential backoff, and every delivery is queryable and replayable via the sender webhook endpoints.
Not all statuses emit webhooks. Intermediate states like initiated, fulfilling, and cancelled do not trigger webhook events.

Compliance holds

payment_order.compliance_hold fires when an on-ramp order’s fiat has been received but Paycrest cannot settle it while a compliance review is open. It is sent at most once per order. Two things make this event different from the rest:
  • It is not a status change. The order deliberately remains pending, so data.status reads pending, not compliance_hold. If you route webhooks by data.status, this event will look like a duplicate pending — branch on event instead.
  • It is terminal in practice. The order will not settle. It stays pending until its virtual account expires, at which point you also receive payment_order.expired. Do not prompt the customer to retry the payment — a second deposit into the same virtual account will be held as well.
Treat it as a signal to stop expecting settlement and to contact Paycrest support about the affected order. What you tell your own customer is your decision.
Not every held order produces this event. Where a hold concerns your account rather than the counterparty, no webhook is sent and you will observe only payment_order.expired.

Webhook Payload (v1)

Webhook Payload (v2)

The v2 payload adds a direction field and uses polymorphic source/destination objects that vary based on whether the order is an off-ramp or on-ramp:

Webhook Verification

Verify the authenticity of webhook payloads using your API Secret to compute an HMAC-SHA256 signature and compare it against the X-Paycrest-Signature header. Always verify against the raw request body bytes — do not parse and re-serialize the JSON, as key ordering may differ. The X-Paycrest-Signature value is a hex string. Use timing-safe comparison on the UTF-8 bytes of the hex strings (Buffer.from(hex, 'utf8')), not Buffer.from(hex, 'hex'). Trim and lowercase the header first.

Timeframes & SLAs

Error Handling

There are two distinct failure paths: expired — No deposit was ever received. The receive address (off-ramp) or virtual account (on-ramp) expired without being funded. The order closes with no funds moved. Common causes:
  • User abandoned the payment flow
  • Deposit was sent to the wrong address or after the address expired
refunded — A deposit was received but fulfillment failed. The deposited funds are automatically returned to the sender. Common causes:
  • No provider available at the requested rate
  • PSP unavailability in the corridor
  • Deposited amount doesn’t match the quoted rate within tolerance
Monitor payment_order.refunded and payment_order.expired webhook events and implement retry logic in your integration. You can also poll GET /v2/sender/orders/:id directly.