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.
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, sodata.statusreadspending, notcompliance_hold. If you route webhooks bydata.status, this event will look like a duplicatepending— branch oneventinstead. - It is terminal in practice. The order will not settle. It stays
pendinguntil its virtual account expires, at which point you also receivepayment_order.expired. Do not prompt the customer to retry the payment — a second deposit into the same virtual account will be held as well.
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 adirection 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 theX-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.
- JavaScript
- Python
- Go
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
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.