Skip to main content
The Sender API lets your app create payment orders in two directions:
  • Offramp — user sends stablecoins, recipient receives local fiat (bank or mobile wallet)
  • Onramp — user deposits fiat into a virtual account, recipient receives stablecoins
Both flows use the same endpoint: POST /v2/sender/orders. Direction comes from the source and destination types you pass. Fastest path to production: one authenticated create call. The API validates bank or mobile account details and resolves an acceptable rate inside that request—you are not required to call verify-account or the public rates URL first. The sections after Create a payment order cover what happens next (response shape, webhooks). Optional: prefetch quotes and account names explains how to polish checkout when you want prefetching—not because it is required.

Flow Overview

Getting Started

1. Obtain API Credentials

Sign up as a Sender at app.paycrest.io and complete the KYB process. Once verified, you’ll have:
  • API Key — included in every request as the API-Key header
  • API Secret — used to verify webhook signatures; keep this secret
const headers = {
  "API-Key": "YOUR_API_KEY",
  "Content-Type": "application/json"
};

2. Configure Tokens (Optional)

In the dashboard settings, you can set defaults per token/network. Offramp: default refundAddress (crypto) and feeAddress; you can also pass source.refundAddress on the request. Onramp: you supply source.refundAccount (fiat bank or mobile details for refunds) in the order payload—configure related defaults in the dashboard where available.

Create a payment order

POST /v2/sender/orders Pass a source and a destination object. The type field on each determines the direction. Offramp and onramp use the same endpoint; examples below include curl for both directions. The extra JavaScript and Python snippets are offramp-shaped—mirror the onramp curl payload (source.type: "fiat", destination.type: "crypto", refundAccount, etc.) in your language.
User sends stablecoins; recipient receives fiat. Set source.type = "crypto" and destination.type = "fiat".
curl -X POST "https://api.paycrest.io/v2/sender/orders" \
  -H "API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": "100",
    "source": {
      "type": "crypto",
      "currency": "USDT",
      "network": "base",
      "refundAddress": "0xYourWalletAddress"
    },
    "destination": {
      "type": "fiat",
      "currency": "NGN",
      "recipient": {
        "institution": "GTBINGLA",
        "accountIdentifier": "1234567890",
        "accountName": "John Doe",
        "memo": "Salary payment"
      }
    },
    "reference": "order-001"
  }'

Request Fields

FieldTypeRequiredDescription
amountstringPayment amount. Denomination set by amountIn (defaults to crypto units).
amountInstring"crypto" (default) or "fiat". Set to "fiat" when amount is in the fiat currency.
ratestringExchange rate (fiat per crypto). Omit to let the API choose a valid rate. Pass rate only when you lock a quote from the user; it must stay within market tolerance. See Prefetch a quote for the UI if you display rates before create.
senderFeePercentstringOptional fee percentage you collect, settled atomically onchain.
referencestringYour internal order ID.
sourceobject{ type: "crypto", ... } for offramp; { type: "fiat", ... } for onramp.
destinationobject{ type: "fiat", ... } for offramp; { type: "crypto", ... } for onramp.

Handle the create response

The create response includes a providerAccount whose shape depends on direction: offramp gives a crypto receive address; onramp gives virtual account / fiat transfer instructions.

Offramp — send tokens to receiveAddress

{
  "id": "550e8400-...",
  "status": "initiated",
  "providerAccount": {
    "network": "base",
    "receiveAddress": "0xProviderReceiveAddress",
    "validUntil": "2026-03-01T10:05:00Z"
  },
  "source": { "type": "crypto", "currency": "USDT", "network": "base" },
  "destination": { "type": "fiat", "currency": "NGN", "recipient": { ... } }
}
Send exactly amount + senderFee + transactionFee (all returned in the response) in the specified token to providerAccount.receiveAddress before validUntil.

Onramp — present virtual account to user

{
  "id": "550e8400-...",
  "status": "initiated",
  "providerAccount": {
    "institution": "Guaranty Trust Bank",
    "accountIdentifier": "0123456789",
    "accountName": "Provider A / John Doe",
    "amountToTransfer": "50000",
    "currency": "NGN",
    "validUntil": "2026-03-01T10:05:00Z"
  },
  "source": { "type": "fiat", "currency": "NGN" },
  "destination": { "type": "crypto", "currency": "USDT", "recipient": { ... } }
}
Display the providerAccount details to your user. They must transfer exactly amountToTransfer in currency to accountIdentifier before validUntil. (There is no onchain token transfer from your app for this path—the user moves fiat through their bank or mobile wallet.)

Monitor completion

Listen for webhooks or poll GET /v2/sender/orders/:id.

Webhook Events

Configure your webhook URL in the Sender Dashboard. Webhooks use the payment_order.<status> format and include a direction field ("offramp" or "onramp"):
{
  "event": "payment_order.settled",
  "webhookVersion": "2",
  "data": {
    "id": "550e8400-...",
    "direction": "offramp",
    "status": "settled",
    "amount": "100",
    "txHash": "0x...",
    ...
  }
}
EventWhen it fires
payment_order.depositedStablecoin deposit detected (offramp)
payment_order.pendingAssigned to provider / fiat deposit confirmed (onramp)
payment_order.validatedFiat payout confirmed by provider
payment_order.settlingOnchain release in progress
payment_order.settledOrder complete
payment_order.refundingRefund in progress
payment_order.refundedFunds returned to sender
payment_order.expiredNo deposit received before validUntil
You can notify the user of a successful offramp at payment_order.validated — that’s when the provider has confirmed fiat delivery. For onramp, use payment_order.settled — that’s when stablecoins are confirmed onchain.

Verify Webhook Signatures

Verify the X-Paycrest-Signature header using HMAC-SHA256 with your API Secret:
const crypto = require('crypto');

function verifyWebhook(rawBody, signature, secret) {
  const computed = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');
  return crypto.timingSafeEqual(Buffer.from(computed), Buffer.from(signature));
}

app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['x-paycrest-signature'];
  if (!verifyWebhook(req.body, sig, process.env.API_SECRET)) {
    return res.status(401).send('Invalid signature');
  }
  const { event, data } = JSON.parse(req.body);
  // handle event...
  res.sendStatus(200);
});

Polling

async function getOrderStatus(orderId) {
  const { data } = await fetch(`https://api.paycrest.io/v2/sender/orders/${orderId}`, {
    headers: { "API-Key": "YOUR_API_KEY" }
  }).then(r => r.json());

  switch (data.status) {
    case 'validated': // offramp: fiat delivered — safe to notify user
    case 'settled':   // onramp: stablecoins delivered — or offramp fully onchain
    case 'refunded':  // funds returned
    case 'expired':   // no deposit received
  }
  return data;
}

Optional: Prefetch quotes and account names

Use this when you want clearer UX before the user confirms—not because the API requires extra calls.

Resolve the account display name early

POST /v2/verify-account runs the same checks again on create, but calling it first cuts down “invalid account” surprises and returns a canonical accountName for your form.
  • Offramp — verify the fiat recipient you will send in destination.recipient (same institution, accountIdentifier, and currency as the order).
  • Onramp — verify source.refundAccount (where fiat refunds go), not the crypto wallet in destination.recipient.
curl -X POST "https://api.paycrest.io/v2/verify-account" \
  -H "API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "institution": "GTBINGLA",
    "accountIdentifier": "1234567890",
    "currency": "NGN"
  }'
A 200 OK response returns the resolved accountName in data. If data is a real name (e.g. "JOHN DOE"), use that exact string as accountName on the order. If data is "OK", name lookup is not available for that corridor—supply your own accountName.

Prefetch a quote for the UI

GET /v2/rates/{network}/{token}/{amount}/{fiat} is public (no API key). Use it when you want to show a rate before submit, or when you will pass rate on create (it must stay within market tolerance). If you omit rate on create, the API still picks an acceptable rate.
The JSON data object includes buy and sell (unless you pass ?side=buy or ?side=sell). Use data.sell.rate for offramp and data.buy.rate for onramp when displaying a quote. provider_id is optional and must be exactly 8 letters (A–Z or a–z) if you pin a provider.
curl "https://api.paycrest.io/v2/rates/base/USDT/100/NGN"

Sender Fees

You can optionally charge your users a fee on each order. It’s settled atomically onchain — no offchain billing required. Configure your fee in Sender Dashboard settings (required). You can override it per-request by passing senderFeePercent in the order payload.
{
  "amount": "100",
  "senderFeePercent": "0.5",
  ...
}
Offramp: the total stablecoin amount the user sends to providerAccount.receiveAddress is amount + senderFee + transactionFee, all returned in the create response. Onramp: the user pays fiat using providerAccount.amountToTransfer (and currency); fee line items in the response still describe what applies on settlement—follow the fields returned for your order.

Testing

Paycrest runs on mainnet only. Test with the minimum order size: $0.50 on any supported chain. Use small amounts until you’ve verified your integration end-to-end. Smoke test: credentials + POST /v2/sender/orders + handle providerAccount + one webhook or poll. Add verify-account and GET /v2/rates when you are polishing the checkout experience—they are not required to go live.

v1 Legacy API

The v1 API is fully supported for existing integrations. It supports offramp only and uses a flat request schema.
POST /v1/sender/orders
{
  "amount": 100,
  "token": "USDT",
  "network": "base",
  "rate": "1500.50",
  "recipient": {
    "institution": "GTBINGLA",
    "accountIdentifier": "1234567890",
    "accountName": "John Doe",
    "currency": "NGN",
    "memo": "Payment"
  },
  "reference": "order-001",
  "returnAddress": "0xYourWalletAddress"
}
The response includes receiveAddress (string) and validUntil. Send tokens to receiveAddress.
Featurev1v2
Offramp
Onramp
SchemaFlat: token, network, recipientPolymorphic: source, destination with type
refundAddressreturnAddress (top-level)source.refundAddress (offramp); onramp uses source.refundAccount
Amount directionAlways cryptoamountIn: "fiat" or "crypto"
Sender feeFixed via dashboardDashboard (fixed) or senderFeePercent on request
Numeric typesNumberString
Webhook version"1""2"