Live Sandbox API

Try every endpoint right from this page — no setup needed

Base URL https://sandbox.fiatsend.com/v1
Demo API Key fs_test_demo_key_2026

Fiatsend Partner API

Enable instant stablecoin-to-mobile-money payouts for your users in Africa. Convert USDC and USDT to Ghana Cedis and deliver funds directly to MTN MoMo, Telecel Cash, and AirtelTigo Money wallets in seconds.

Instant Conversion

Fast settlement from stablecoin to mobile money wallet.

3 Mobile Networks

MTN MoMo, Telecel Cash, and AirtelTigo Money across Ghana.

On-Chain Verification

Every transaction is anchored on-chain with a verifiable hash.

Real-Time Webhooks

Get instant status callbacks for every withdrawal state change.

Base URLs

EnvironmentBase URL
Sandbox https://sandbox.fiatsend.com/v1
Production https://api.fiatsend.com/v1

Rate Limits

PlanRequests / minRequests / day
Sandbox6010,000
Production300100,000

Authentication

All API requests require a Bearer token in the Authorization header. API keys are issued during partner onboarding.

HTTP Header
Authorization: Bearer your_api_key_here
Getting API Keys

Create your keys in console.fiatsend.com under API Keys. Sandbox keys start with fs_test_; production keys start with fs_live_.

Example Request

bash
curl -X GET "https://sandbox.fiatsend.com/v1/health" \
  -H "Authorization: Bearer fs_test_abc123..."
javascript
const response = await fetch("https://sandbox.fiatsend.com/v1/health", {
  headers: {
    "Authorization": "Bearer fs_test_abc123...",
    "Content-Type": "application/json"
  }
});

const data = await response.json();
console.log(data);

Quick Start

Follow these five steps to make your first stablecoin-to-mobile-money payout.

1

Get API Credentials

Generate a sandbox API key in console.fiatsend.com under API Keys.

2

Check Supported Networks

Query the available mobile money networks and their current status.

bash
curl -X GET "https://sandbox.fiatsend.com/v1/supported-networks" \
  -H "Authorization: Bearer fs_test_abc123..."
javascript
const res = await fetch("https://sandbox.fiatsend.com/v1/supported-networks", {
  headers: { "Authorization": "Bearer fs_test_abc123..." }
});
const { data } = await res.json();
console.log(data);
// [{ network_id: "MTN", name: "MTN MoMo", status: "operational", ... }]
3

Get a Rate Quote

Check the current FX rate before creating a withdrawal. Rates are guaranteed until valid_until.

bash
curl -X GET "https://sandbox.fiatsend.com/v1/rates?from_currency=USDC&to_currency=GHS&amount=100.00" \
  -H "Authorization: Bearer fs_test_abc123..."
javascript
const params = new URLSearchParams({
  from_currency: "USDC",
  to_currency: "GHS",
  amount: "100.00"
});

const res = await fetch(`https://sandbox.fiatsend.com/v1/rates?${params}`, {
  headers: { "Authorization": "Bearer fs_test_abc123..." }
});
const { data } = await res.json();
console.log(`Rate: ${data.rate} GHS per USDC, valid until ${data.valid_until}`);
4

Create a Withdrawal

Initiate the payout. Fiatsend locks the rate, converts USDC to GHS, and sends funds to the mobile wallet.

bash
curl -X POST "https://sandbox.fiatsend.com/v1/withdrawals" \
  -H "Authorization: Bearer fs_test_abc123..." \
  -H "Content-Type: application/json" \
  -d '{
    "amount": "50.00",
    "currency": "USDC",
    "recipient_phone": "+233241234567",
    "mobile_network": "MTN",
    "reference_id": "acme-txn-abc123",
    "metadata": {
      "contractor_id": "con_12345",
      "invoice_id": "inv_67890"
    }
  }'
javascript
const res = await fetch("https://sandbox.fiatsend.com/v1/withdrawals", {
  method: "POST",
  headers: {
    "Authorization": "Bearer fs_test_abc123...",
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    amount: "50.00",
    currency: "USDC",
    recipient_phone: "+233241234567",
    mobile_network: "MTN",
    reference_id: "acme-txn-abc123",
    metadata: {
      contractor_id: "con_12345",
      invoice_id: "inv_67890"
    }
  })
});

const { data } = await res.json();
console.log(`Withdrawal ${data.withdrawal_id} created — status: ${data.status}`);
5

Handle the Webhook Callback

Fiatsend sends a POST request to your registered webhook URL when the withdrawal status changes. See Webhooks for details.

json
{
  "id": "evt_3f9a1c7d2b8e",
  "type": "withdrawal.completed",
  "created_at": "2026-03-21T06:59:42Z",
  "data": {
    "withdrawal_id": "wd_9f8e7d6c5b4a",
    "status": "completed",
    "amount": "50.00",
    "currency": "USDC",
    "ghs_amount": "742.50",
    "recipient_phone": "+233241234567",
    "mobile_network": "MTN",
    "reference_id": "acme-txn-abc123"
  }
}

Withdrawals

Create and manage stablecoin-to-mobile-money withdrawals.

POST /v1/withdrawals

Create a Withdrawal

Initiate a stablecoin-to-mobile-money withdrawal. Fiatsend locks the FX rate at the time of request, converts the stablecoin amount to GHS, and delivers funds to the specified mobile money wallet.

The withdrawal goes through the following statuses:

  • pending — Request received, awaiting processing
  • processing — On-chain transaction submitted, mobile money transfer initiated
  • completed — Funds delivered to mobile money wallet
  • failed — Transaction failed (funds returned to source)

Request Body

FieldTypeRequiredDescription
amountstringYesAmount in source currency (e.g., "50.00")
currencystringYesSource stablecoin: USDC or USDT
recipient_phonestringYesRecipient's phone in E.164 format (e.g., +233241234567)
mobile_networkstringYesMTN, TELECEL, or AIRTELTIGO
reference_idstringYesYour unique transaction reference (idempotency key)
metadataobjectNoOptional key-value metadata

Request

json
{
  "amount": "50.00",
  "currency": "USDC",
  "recipient_phone": "+233241234567",
  "mobile_network": "MTN",
  "reference_id": "acme-txn-abc123",
  "metadata": {
    "contractor_id": "con_12345",
    "invoice_id": "inv_67890"
  }
}

Response 201

json
{
  "status": "success",
  "data": {
    "withdrawal_id": "wd_9f8e7d6c5b4a",
    "status": "pending",
    "amount": "50.00",
    "currency": "USDC",
    "ghs_amount": "742.50",
    "fx_rate": "15.00",
    "fee": "0.50",
    "recipient_phone": "+233241234567",
    "mobile_network": "MTN",
    "reference_id": "acme-txn-abc123",
    "estimated_completion": "2026-03-21T07:00:00Z",
    "created_at": "2026-03-21T06:59:30Z"
  }
}

Error Response 400

json
{
  "status": "error",
  "code": "INVALID_PHONE",
  "message": "Phone number must be a valid Ghana number starting with +233"
}
GET /v1/withdrawals/{withdrawal_id}

Get Withdrawal Status

Retrieve the current status and details of a withdrawal. For real-time updates, we recommend using webhooks instead of polling.

Path Parameters

ParameterTypeRequiredDescription
withdrawal_idstringYesThe withdrawal ID (e.g., wd_9f8e7d6c5b4a)

Response 200

json
{
  "status": "success",
  "data": {
    "withdrawal_id": "wd_9f8e7d6c5b4a",
    "status": "completed",
    "amount": "50.00",
    "currency": "USDC",
    "ghs_amount": "742.50",
    "fx_rate": "15.00",
    "fee": "0.50",
    "recipient_phone": "+233241234567",
    "mobile_network": "MTN",
    "reference_id": "acme-txn-abc123",
    "on_chain_tx_hash": "0x1234abcd...",
    "mobile_money_reference": "MOMO-REF-456789",
    "created_at": "2026-03-21T06:59:30Z",
    "processing_at": "2026-03-21T06:59:35Z",
    "completed_at": "2026-03-21T06:59:42Z"
  }
}
GET /v1/transactions

List Transactions

Retrieve a paginated list of transactions with optional filters.

Query Parameters

ParameterTypeRequiredDescription
statusstringNoFilter by status: pending, processing, completed, failed
from_datedate-timeNoStart date filter (ISO 8601)
to_datedate-timeNoEnd date filter (ISO 8601)
reference_idstringNoReturn only the withdrawal created with this reference_id. Re-posting a reference_id to POST /v1/withdrawals returns the existing withdrawal (HTTP 200), so retries are safe.
pageintegerNoPage number (default: 1)
per_pageintegerNoResults per page (default: 25, max: 100)

Response 200

json
{
  "status": "success",
  "data": [
    {
      "withdrawal_id": "wd_9f8e7d6c5b4a",
      "status": "completed",
      "amount": "50.00",
      "currency": "USDC",
      "ghs_amount": "742.50",
      "fx_rate": "15.00",
      "recipient_phone": "+233241234567",
      "mobile_network": "MTN",
      "reference_id": "acme-txn-abc123",
      "created_at": "2026-03-21T06:59:30Z",
      "completed_at": "2026-03-21T06:59:42Z"
    }
  ],
  "pagination": {
    "page": 1,
    "per_page": 25,
    "total": 1,
    "total_pages": 1
  }
}

Checkout Sessions

Take payments on your own website or app. Your server creates a checkout session for the exact amount, sends the customer to the hosted checkout, and Fiatsend notifies your server when it's paid. Customers can pay with Fiatsend checkout, mobile money or bank transfer.

Test and live keys use the same address

Create sessions with a fs_test_… key to test (no real money moves, and Fiatsend checkout uses the Stellar test network) and a fs_live_… key for live payments, which need an activated account. Get keys in Console under Developers → API Keys. The shared demo key used by "Try it" elsewhere in this portal does not work for checkout sessions, so this section has no Try it buttons. Keep keys on your server.

Prefer no code?

Payment buttons, an embeddable checkout and the Console guide are covered in the Website Checkout guide.

POST /v1/checkout/sessions

Create Checkout Session

Create a checkout for one customer's payment. The session stays open until it is paid or cancelled.

Request Body

FieldTypeRequiredDescription
amountnumberYesPrice in major units, e.g. 50.00. Must be more than 0.
currencystringNoGHS (default) or USDC.
descriptionstringNoWhat the customer is paying for (up to 200 characters). Shown on the checkout page and receipt.
client_reference_idstringNoYour own order ID (up to 100 characters). Returned on the session and in webhooks.
customerobjectNoOptional name, email and phone (E.164, e.g. +233241234567). With an email the customer receives a receipt by email.
success_urlstringNoWhere to send the customer after paying. https:// only (http://localhost allowed for testing). {SESSION_ID} is replaced with the session ID; without a placeholder, session_id is added as a query parameter.
cancel_urlstringNoWhere to send the customer if they go back without paying. Same rules as success_url.
metadataobjectNoUp to 20 string key/value pairs (keys up to 40 characters, values up to 500), returned unchanged.
accepted_methodsstring[]NoAny of fiatsend, mobile_money (GHS only), bank_transfer. Defaults to the methods saved in the business's Payment settings.

Example

bash
curl -X POST "https://api.fiatsend.com/v1/checkout/sessions" \
  -H "Authorization: Bearer fs_test_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 50.00,
    "currency": "GHS",
    "description": "Order 1042",
    "client_reference_id": "order_1042",
    "customer": { "email": "ama@example.com" },
    "success_url": "https://yourshop.com/thanks?session={SESSION_ID}",
    "cancel_url": "https://yourshop.com/cart",
    "metadata": { "plan": "basic" }
  }'
javascript
// Run on your server. Never expose your API key in browser code.
const res = await fetch("https://api.fiatsend.com/v1/checkout/sessions", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.FIATSEND_API_KEY}`,
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    amount: 50.0,
    currency: "GHS",
    description: "Order 1042",
    client_reference_id: "order_1042",
    success_url: "https://yourshop.com/thanks?session={SESSION_ID}",
    cancel_url: "https://yourshop.com/cart"
  })
});
const session = await res.json();
// Redirect the customer to session.url
python
import os, requests

res = requests.post(
    "https://api.fiatsend.com/v1/checkout/sessions",
    headers={"Authorization": f"Bearer {os.environ['FIATSEND_API_KEY']}"},
    json={
        "amount": 50.0,
        "currency": "GHS",
        "description": "Order 1042",
        "client_reference_id": "order_1042",
        "success_url": "https://yourshop.com/thanks?session={SESSION_ID}",
        "cancel_url": "https://yourshop.com/cart",
    },
)
session = res.json()  # redirect the customer to session["url"]

Response 201

json
{
  "id": "cs_5f1c8a2e9d7b4c3a6e0f1b2c3d4e5f60718293a4b5c6d7e8",
  "object": "checkout.session",
  "status": "open",
  "url": "https://console.fiatsend.com/i/5f1c8a2e9d7b4c3a6e0f1b2c3d4e5f60718293a4b5c6d7e8",
  "amount": 50,
  "currency": "GHS",
  "description": "Order 1042",
  "client_reference_id": "order_1042",
  "metadata": { "plan": "basic" },
  "customer": { "name": null, "email": "ama@example.com", "phone": null },
  "accepted_methods": ["fiatsend"],
  "success_url": "https://yourshop.com/thanks?session={SESSION_ID}",
  "cancel_url": "https://yourshop.com/cart",
  "paid_at": null,
  "paid_via": null,
  "livemode": false,
  "created_at": "2026-10-08T09:00:00.000Z"
}
GET /v1/checkout/sessions/{session_id}

Retrieve Checkout Session

Returns one session. A status of complete means it is paid; use this (or the webhook below) to confirm payment before you fulfil an order. Never rely on a browser event alone.

Response 200

The session object.

GET /v1/checkout/sessions?limit={n}

List Checkout Sessions

Your most recent sessions, newest first, in the mode of the key you use. limit is 1 to 100 (default 25).

Response 200

json
{
  "object": "list",
  "data": [
    { "id": "cs_5f1c…", "object": "checkout.session", "status": "complete", "amount": 50, "currency": "GHS", "...": "..." }
  ]
}

Session Object

FieldTypeDescription
idstringSession ID, e.g. cs_….
objectstringAlways checkout.session.
statusstringopen, complete or cancelled.
urlstringThe hosted checkout page. Send the customer here.
amountnumberAmount in major units.
currencystringGHS or USDC.
descriptionstring | nullDescription you supplied.
client_reference_idstring | nullYour order ID.
metadataobjectYour metadata, unchanged.
customerobjectname, email, phone (each may be null).
accepted_methodsstring[]Methods offered on this checkout.
success_url / cancel_urlstring | nullYour return URLs, as supplied.
paid_atstring | nullISO 8601 time the payment was confirmed.
paid_viastring | nullfiatsend, mobile_money or bank_transfer once paid, or manual if the business marked it paid itself.
livemodebooleantrue for live keys, false for test keys.
created_atstringISO 8601 creation time.

Webhook: checkout.session.completed

Sent to your server the moment a session is paid, even if the customer has closed the page. Register an endpoint in Console under Developers → Webhooks and tick checkout.session.completed. Endpoints must be public https:// URLs.

json
{
  "id": "evt_9a8b7c6d5e4f3a2b1c0d9e8f",
  "event": "checkout.session.completed",
  "sentAt": "2026-10-08T09:02:11.000Z",
  "livemode": true,
  "data": {
    "object": {
      "id": "cs_5f1c…",
      "object": "checkout.session",
      "status": "complete",
      "amount": 50,
      "currency": "GHS",
      "client_reference_id": "order_1042",
      "metadata": { "plan": "basic" },
      "paid_at": "2026-10-08T09:02:09.000Z",
      "paid_via": "fiatsend",
      "livemode": true
    }
  }
}
  • Signature. X-Fiatsend-Signature is the HMAC-SHA256 (hex) of the raw request body using your webhook secret, exactly as described in the Webhook Guide. X-Fiatsend-Event carries the event name.
  • Payload shape. The event name is in the event field and the full session is in data.object. This differs from the withdrawal events above, which use type.
  • Delivery. Up to 3 attempts in total (about 1 second, then 4 seconds apart), each with an 8 second timeout. Reply with any 2xx to acknowledge. Delivery is at-least-once, so de-duplicate on the event id.

Redirects and the Embedded Checkout

After payment the customer is returned to your success_url; if they go back, to your cancel_url. To open checkout in a popup on your page instead of redirecting, load Fiatsend's script (links must point to console.fiatsend.com). If the popup is blocked, the customer is redirected so they can still pay.

html
<script src="https://console.fiatsend.com/fiatsend-checkout.js"></script>
<button data-fiatsend-checkout="YOUR_BUTTON_ID">Pay with Fiatsend</button>

<script>
  // Or open a session you created on your server
  Fiatsend.checkout.open({ session: "cs_..." });
  Fiatsend.checkout.on("paid", function (e) { /* UI hint only: confirm on your server */ });
</script>

Errors

Checkout endpoints return errors in this shape (note: different from the status/code shape used by other endpoints):

json
{ "error": { "code": "invalid_request", "message": "amount: amount must be more than 0" } }
HTTPCodeMeaning
400invalid_requestA field is invalid. The message names it, e.g. amount: amount must be more than 0.
401—API key missing, malformed or invalid. Body: {"message": "..."}.
403account_not_activeA live key was used before the account was activated. Use a test key until you go live.
404not_foundNo such checkout session for this key's mode.
422account_not_readyThe business can't take that payment yet, e.g. no Stellar wallet is connected for fiatsend checkout.
429rate_limitedToo many requests. Retry shortly.
502upstream_unavailableCheckout is temporarily unavailable. Retry.

Limits

Checkout endpoints allow 120 requests per minute per business.

Payment Intents Beta

Accept consumer-approved payments from Fiatsend users. Merchants create intents and the consumer approves or rejects in-app.

Beta

Payment intents are in beta: they work in sandbox and production, but fields and events may still change. For a stable way to take payments, use Checkout Sessions.

POST /v1/payment-intents

Create Payment Intent

Create a merchant payment request for a specific consumer phone number.

Request Body

FieldTypeRequiredDescription
amountstringYesAmount requested (e.g. "25.00")
currencystringYesSettlement currency, typically GHS
consumer_phonestringYesConsumer phone in E.164 format
merchant_referencestringYesUnique merchant reference for idempotency
terminal_idstringNoMerchant terminal identifier
descriptionstringNoOptional checkout description

Response 201

json
{
  "status": "success",
  "data": {
    "payment_intent_id": "pi_4f7a3a2d",
    "status": "pending_approval",
    "amount": "25.00",
    "currency": "GHS",
    "consumer_phone": "+233201234567",
    "merchant_reference": "checkout-2026-04-30-001",
    "terminal_id": "FS-B4E8E8598E9B",
    "description": "Order #10293",
    "created_at": "2026-04-30T08:10:00Z",
    "expires_at": "2026-04-30T08:11:30Z"
  }
}
GET /v1/payment-intents/{payment_intent_id}

Get Payment Intent

Fetch current status of a payment intent.

POST /v1/payment-intents/{payment_intent_id}/cancel

Cancel Payment Intent

Cancel a pending payment request from the merchant side before consumer approval.

Payment Intent Events

Registered webhooks receive payment_intent.pending_approval, payment_intent.approved, payment_intent.completed, payment_intent.rejected, payment_intent.cancelled, payment_intent.expired and payment_intent.failed, signed the same way as withdrawal events.

Rates

Get real-time FX rates for stablecoin to GHS conversion.

GET /v1/rates

Get Current FX Rate

Get a real-time quote for converting stablecoins to GHS. The returned rate includes a valid_until timestamp — the rate is guaranteed until that time.

Query Parameters

ParameterTypeRequiredDescription
from_currencystringYesUSDC or USDT
to_currencystringYesGHS
amountstringYesAmount in source currency (e.g., "100.00")

The fee is in the source stablecoin (1% of amount), and total_ghs = (amount − fee) × rate. Withdrawals return the same values as fee and ghs_amount.

Response 200

json
{
  "status": "success",
  "data": {
    "from_currency": "USDC",
    "to_currency": "GHS",
    "amount": "100.00",
    "rate": "15.02",
    "fee": "1.00",
    "total_ghs": "1486.98",
    "valid_until": "2026-03-21T07:05:00Z"
  }
}

Networks

Query supported mobile money networks and their status.

GET /v1/supported-networks

List Supported Networks

Returns all available mobile money networks with their current operational status.

Response 200

json
{
  "status": "success",
  "data": [
    {
      "network_id": "MTN",
      "name": "MTN MoMo",
      "country": "GH",
      "currency": "GHS",
      "status": "operational",
      "min_amount": "50.00",
      "max_amount": "15000.00"
    },
    {
      "network_id": "TELECEL",
      "name": "Telecel Cash",
      "country": "GH",
      "currency": "GHS",
      "status": "operational",
      "min_amount": "50.00",
      "max_amount": "15000.00"
    },
    {
      "network_id": "AIRTELTIGO",
      "name": "AirtelTigo Money",
      "country": "GH",
      "currency": "GHS",
      "status": "operational",
      "min_amount": "50.00",
      "max_amount": "15000.00"
    }
  ]
}

min_amount and max_amount are in the network's currency (GHS): every payout must deliver between GHS 50 and GHS 15,000 (ghs_amount) on any network, otherwise the request returns BELOW_MINIMUM or ABOVE_MAXIMUM. Payouts are also subject to your tier limits (GET /v1/limits: basic, standard, enterprise).

GET /v1/limits

Get Transaction Limits

Returns transaction limits per KYC tier.

Response 200

json
{
  "status": "success",
  "data": [
    {
      "tier": "basic",
      "daily_limit": "500.00",
      "monthly_limit": "5000.00",
      "per_transaction_max": "200.00"
    },
    {
      "tier": "standard",
      "daily_limit": "5000.00",
      "monthly_limit": "50000.00",
      "per_transaction_max": "2000.00"
    },
    {
      "tier": "enterprise",
      "daily_limit": "50000.00",
      "monthly_limit": "500000.00",
      "per_transaction_max": "10000.00"
    }
  ]
}

Webhook Endpoints

Register and manage webhook endpoints for status callbacks.

POST /v1/webhooks

Register a Webhook

Register a URL to receive real-time status updates for withdrawals. All webhook payloads are signed with HMAC-SHA256.

Request Body

FieldTypeRequiredDescription
urlstring (URI)YesYour webhook endpoint URL
eventsstring[]YesEvents to subscribe to
secretstringYesSecret for HMAC-SHA256 signature verification

Request

json
{
  "url": "https://api.example.com/webhooks/fiatsend",
  "events": [
    "withdrawal.completed",
    "withdrawal.failed",
    "withdrawal.processing"
  ],
  "secret": "whsec_your_secret_here"
}

Response 201

json
{
  "status": "success",
  "data": {
    "webhook_id": "wh_abc123",
    "url": "https://api.example.com/webhooks/fiatsend",
    "events": ["withdrawal.completed", "withdrawal.failed", "withdrawal.processing"],
    "active": true,
    "created_at": "2026-03-21T07:00:00Z"
  }
}
GET /v1/webhooks

List Registered Webhooks

Retrieve all webhook endpoints registered for your account.

Response 200

json
{
  "status": "success",
  "data": [
    {
      "webhook_id": "wh_abc123",
      "url": "https://api.example.com/webhooks/fiatsend",
      "events": ["withdrawal.completed", "withdrawal.failed"],
      "active": true,
      "created_at": "2026-03-21T07:00:00Z"
    }
  ]
}
DELETE /v1/webhooks/{webhook_id}

Delete a Webhook

Remove a registered webhook endpoint.

Path Parameters

ParameterTypeRequiredDescription
webhook_idstringYesThe webhook ID to delete

Response 204

No content — webhook successfully deleted.

Register a webhook with POST /v1/webhooks above, then delete it here using the last registered webhook_id.

Health

Service health and status checks. This endpoint does not require authentication.

GET /v1/health

Service Health Check

Returns service status, uptime, and API version. No authentication required.

Response 200

json
{
  "status": "healthy",
  "version": "1.3.0",
  "uptime_seconds": 864000,
  "environment": "sandbox",
  "services": {
    "blockchain": "operational",
    "mobile_money": "operational",
    "database": "operational"
  }
}

Webhooks

Fiatsend sends real-time notifications to your server when withdrawal statuses change. Instead of polling the API, register a webhook URL and we'll push updates to you.

How It Works

  1. Register a webhook endpoint via POST /v1/webhooks
  2. Fiatsend sends a POST request to your URL whenever a subscribed event occurs
  3. Your server verifies the signature and processes the event
  4. Respond with a 2xx status code to acknowledge receipt
Retry Policy

If your endpoint returns a non-2xx status or times out (30s), Fiatsend retries with exponential backoff: 1 min, 5 min, 30 min, 2 hr, 24 hr. After 5 failed attempts, the webhook is marked as failed.

Webhook Payload

Each delivery is a POST with headers X-Fiatsend-Signature: sha256=<hex>, X-Fiatsend-Event (the event type) and X-Fiatsend-Delivery (unique per attempt). Use id to deduplicate: retries resend the same event id. Delivery is attempted up to 3 times.

json
{
  "id": "evt_3f9a1c7d2b8e",
  "type": "withdrawal.completed",
  "created_at": "2026-03-21T06:59:42Z",
  "data": {
    "withdrawal_id": "wd_9f8e7d6c5b4a",
    "status": "completed",
    "amount": "50.00",
    "currency": "USDC",
    "ghs_amount": "742.50",
    "fx_rate": "15.00",
    "fee": "0.50",
    "recipient_phone": "+233241234567",
    "mobile_network": "MTN",
    "reference_id": "acme-txn-abc123",
    "on_chain_tx_hash": "0x1234abcd...",
    "mobile_money_reference": "MOMO-REF-456789",
    "completed_at": "2026-03-21T06:59:42Z"
  }
}

Signature Verification

Every webhook request includes an X-Fiatsend-Signature header containing an HMAC-SHA256 signature of the request body, computed using your webhook secret. Always verify this signature before processing events.

javascript
const crypto = require('crypto');

// X-Fiatsend-Signature is sha256=<hex HMAC-SHA256 of the raw body>.
function verifyWebhookSignature(rawBody, header, secret) {
  if (!header) return false;
  const received = header.startsWith('sha256=') ? header.slice(7) : header;
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  const a = Buffer.from(received, 'hex');
  const b = Buffer.from(expected, 'hex');
  // timingSafeEqual throws on different lengths, so check first and answer 401, not 500.
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// Express middleware example
app.post('/webhooks/fiatsend', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-fiatsend-signature'];
  const isValid = verifyWebhookSignature(req.body, signature, process.env.WEBHOOK_SECRET);

  if (!isValid) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  const event = JSON.parse(req.body);
  const type = event.type ?? event.event; // withdrawal events use type

  // Deduplicate on event.id: retries reuse the same id.
  switch (type) {
    case 'withdrawal.completed':
      // Mark payout as complete in your system
      break;
    case 'withdrawal.failed':
      // Handle failure, notify user
      break;
  }

  res.status(200).json({ received: true });
});

Event Types

EventDescription
withdrawal.pendingWithdrawal request received, awaiting processing
withdrawal.processingOn-chain transaction submitted, mobile money transfer initiated
withdrawal.completedFunds successfully delivered to mobile money wallet
withdrawal.failedTransaction failed — funds returned to source
checkout.session.completedA checkout session was paid. Uses a different payload shape; see Checkout Sessions.

Checkout webhooks are registered in Console (Developers → Webhooks) rather than with POST /v1/webhooks, put the event name in event with the session in data.object, and are retried up to 3 times. The signature scheme above is identical. See Checkout Sessions.

Error Codes

Error responses carry a machine-readable code and a human-readable message. Payouts, rates, networks, webhooks and payment intents use the shape below. Checkout sessions use {"error": {"code": "...", "message": "..."}}, so handle both: body.code ?? body.error?.code.

json
{
  "status": "error",
  "code": "INVALID_PHONE",
  "message": "Phone number must be a valid Ghana number starting with +233"
}
CodeHTTP StatusDescription
UNAUTHORIZED 401 Invalid or missing API key
INVALID_PHONE 400 Invalid phone number format
INVALID_NETWORK 400 Unsupported mobile money network
BELOW_MINIMUM 422 Payout below GHS 50 (the cedis delivered)
ABOVE_MAXIMUM 422 Payout above GHS 15,000 (the cedis delivered)
REFERENCE_IN_USE 409 This reference_id is already used; use a unique one per payout
INSUFFICIENT_BALANCE 422 Your balance doesn't cover the withdrawal; fund it in the console
PAYOUTS_UNAVAILABLE 503 Mobile money payouts are temporarily unavailable; nothing was charged, retry later
RATE_EXPIRED 422 Quoted rate has expired
NETWORK_DOWN 503 Mobile money network unavailable
INSUFFICIENT_LIQUIDITY 503 Insufficient liquidity for conversion
RATE_LIMITED 429 Too many requests
INTERNAL_ERROR 500 Internal server error

SDKs & Tools

Libraries and tools to accelerate your integration.

Node.js SDK

Temporarily unavailable for Partner API integration.

Unavailable

Python SDK

Temporarily unavailable for Partner API integration.

Unavailable

Postman Collection

Import our collection to test all endpoints.

Download Collection

OpenAPI Spec

Full API specification in OpenAPI 3.0 format.

Download YAML

AI Tools

Building with an AI coding assistant? Give it the Fiatsend rules (sandbox first, string amounts, reference_id idempotency, webhook signatures) so the code it writes is right the first time. Everything lives in the fiatsend-ai kit.

Copies the llms-full.txt link: the whole API reference in one AI-readable file. Short index: llms.txt.

Which Piece for Which Tool

AI toolAdd this
Claude CodeCopy skills/fiatsend-api/ into .claude/skills/ (or ~/.claude/skills/)
Claude.ai / Claude DesktopUpload the skills/fiatsend-api folder as a skill
CursorCopy .cursor/rules/fiatsend.mdc into your project's .cursor/rules/
VS Code + Copilot, Windsurf, OpenAI Codex, ZedCopy AGENTS.md to your repo root
ChatGPT or any chat AIPaste the llms-full.txt link

MCP Server (Live API Tools)

The kit's MCP server lets your AI tool check rates and networks, preview and track payouts, create checkout sessions and debug webhook signatures while you work. It's sandbox-first: fs_test_ keys can't move real money, live payouts stay off unless FIATSEND_ALLOW_LIVE_PAYOUTS=true is set, and every payout needs explicit confirmation.

The npm package @fiatsend/mcp is coming soon. Until then, build it from the kit:

bash
git clone https://github.com/fiatsend/fiatsend-ai.git
cd fiatsend-ai/mcp-server && npm install && npm run build

Then add it to your tool's MCP config (Claude Desktop, Cursor .cursor/mcp.json, Windsurf), using the full path to the kit:

json
{
  "mcpServers": {
    "fiatsend": {
      "command": "node",
      "args": ["/path/to/fiatsend-ai/mcp-server/dist/index.js"],
      "env": { "FIATSEND_API_KEY": "fs_test_xxx" }
    }
  }
}

Check it works by asking your AI tool to "run fiatsend_health": it should report key_mode: test and the sandbox URL.

Support

Need help integrating? Our engineering team is here to support you.

Website

fiatsend.com

Status Page

status.fiatsend.com Coming Soon