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

Offramp 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 (offramp) or virtual account (onramp) 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.

Onramp Lifecycle (Fiat → Stablecoin)

1

initiated

Sender creates an onramp 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:
StatusDescription
initiatedOrder created, awaiting deposit
depositedDeposit confirmed (offramp only)
pendingAssigned to provider, awaiting fulfillment
fulfillingProvider is disbursing fiat or stablecoins
fulfilledFulfillment completed by provider (internal)
validatedPayout confirmed
settlingOnchain settlement in progress
settledFully complete
cancelledOrder cancelled
refundingRefund in progress (deposit received, fulfillment failed)
refundedFunds returned to sender
expiredReceive address or virtual account expired without receiving a deposit

Webhook Events

Paycrest sends webhooks to your configured endpoint as an order progresses. The event name format is payment_order.<status>.
EventTriggered When
payment_order.depositedDeposit confirmed on the receive address
payment_order.pendingOrder assigned to a provider
payment_order.validatedPayout confirmed by provider
payment_order.settlingOnchain settlement started
payment_order.settledSettlement complete
payment_order.refundingRefund initiated
payment_order.refundedRefund complete
payment_order.expiredOrder expired after 5 minutes
Not all statuses emit webhooks. Intermediate states like initiated, fulfilling, and cancelled do not trigger webhook events.

Webhook Payload (v1)

{
  "event": "payment_order.settled",
  "webhookVersion": "1",
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "amount": "100",
    "amountInUsd": "100.00",
    "amountPaid": "100",
    "amountReturned": "0",
    "percentSettled": "100",
    "senderFee": "0.5",
    "networkFee": "0.01",
    "rate": "1500.50",
    "network": "base",
    "gatewayId": "0xabc123...",
    "senderId": "sender-uuid",
    "recipient": {
      "institution": "GTBINGLA",
      "accountIdentifier": "1234567890",
      "accountName": "John Doe",
      "currency": "NGN",
      "memo": "Payment"
    },
    "fromAddress": "0xsender...",
    "returnAddress": "0xrefund...",
    "reference": "payment-123",
    "txHash": "0xtxhash...",
    "status": "settled",
    "updatedAt": "2026-03-01T10:30:00Z",
    "createdAt": "2026-03-01T10:29:35Z"
  }
}

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 offramp or onramp:
{
  "event": "payment_order.settled",
  "webhookVersion": "2",
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "direction": "offramp",
    "amount": "100",
    "amountInUsd": "100.00",
    "amountPaid": "100",
    "amountReturned": "0",
    "percentSettled": "100",
    "senderFee": "0.5",
    "transactionFee": "0.01",
    "rate": "1500.50",
    "gatewayId": "0xabc123...",
    "senderId": "sender-uuid",
    "source": {
      "type": "crypto",
      "currency": "USDT",
      "network": "base"
    },
    "destination": {
      "type": "fiat",
      "currency": "NGN",
      "recipient": {
        "institution": "GTBINGLA",
        "accountIdentifier": "1234567890",
        "accountName": "John Doe",
        "memo": "Payment"
      }
    },
    "fromAddress": "0xsender...",
    "reference": "payment-123",
    "txHash": "0xtxhash...",
    "status": "settled",
    "timestamp": "2026-03-01T10:30:00Z"
  }
}

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:
const crypto = require('crypto');

function verifyWebhook(rawBody, signature, secret) {
  const computed = crypto
    .createHmac('sha256', secret)
    .update(rawBody) // rawBody is the raw request body Buffer/string
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(computed),
    Buffer.from(signature)
  );
}

Timeframes & SLAs

MetricTarget
Median order completion time (NGN)< 30 seconds
Reliability (NGN)≥ 95% complete within 30s
Refund rate≤ 3%

Error Handling

There are two distinct failure paths: expired — No deposit was ever received. The receive address (offramp) or virtual account (onramp) 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.