Merchant API

Public Integration Guide

How to connect your main site to this payment proxy: create a payment, send the customer to card checkout, receive webhooks, and handle the return URL. The exchange provider is not exposed to your backend.

API Documentation

Server-to-server integration for merchants (your hidden main site).

Overview

1. Your backend calls POST /api/merchant/payments/init (signed) and gets checkout_url.

2. Redirect the customer to checkout_url on this proxy domain.

3. Customer pays by card (fiat → crypto); they are redirected to the payment partner page.

4. This proxy sends a webhook to your URL when status changes.

5. Customer returns to your return_success_url / return_cancel_url with a signed query (UX only).

Important: mark the order paid and deliver the product only on webhook newStatus: PAID, not on browser redirect alone.

1. Credentials

You receive from the proxy operator:

  • merchant_api_key — Bearer token for init requests
  • merchant_webhook_secret — sign init requests and verify webhooks + return URL
  • Allowlisted hosts for return_success_url and return_cancel_url

Do not use SimpleSwap or other provider API keys on your main site.

2. Authentication (init)

Headers

Authorization: Bearer YOUR_MERCHANT_API_KEY
Content-Type: application/json
X-Timestamp: 1717580000
X-Signature: sha256=<hex>

Signature: HMAC_SHA256(secret, X-Timestamp + "." + raw_json_body), header value sha256=<hex>.

Clock skew tolerance: 300 seconds.

3. Create payment (init)

POST /api/merchant/payments/init

Request body

{
  "merchant_order_id": "order-12345",
  "amount_receive": "100.00",
  "ticker_to": "usdt",
  "network_to": "trc20",
  "address_to": "TYourMerchantWallet...",
  "extra_id_to": "",
  "return_success_url": "https://main-site.example/order/12345/success",
  "return_cancel_url": "https://main-site.example/order/12345/cancel",
  "merchant_webhook_url": "https://main-site.example/api/webhooks/cryptopay",
  "metadata": { "orderId": "12345" },
  "customer_email": "user@example.com",
  "description": "Order #12345",
  "fiat_currency": "usd",
  "fixed": false,
  "expires_in_seconds": 3600
}

amount_receive — how much crypto must arrive on address_to (merchant wallet).

fiat_currency — card payment currency: usd, eur, or gbp (default usd).

metadata — optional object (max 5 KB), echoed back in webhooks unchanged.

provider — optional; default simpleswap.

Response

{
  "ok": true,
  "payment_id": "550e8400-e29b-41d4-a716-446655440000",
  "provider": "simpleswap",
  "checkout_url": "https://this-proxy.example/pay/a8f3...64charToken...",
  "status": "CREATED",
  "expires_at": "2026-06-05T13:00:00Z"
}

Checkout redirect: HTTP 302 the customer to checkout_url. They complete card payment on the partner page linked from our checkout.

Deprecated: POST /api/payment/create returns 410 — use init above.

4. Receive webhooks

Configure your endpoint URL in init (merchant_webhook_url) or ask the operator for a default. We POST on status changes.

Idempotency: store X-Webhook-Id and ignore duplicates.

Delivery: respond with 2xx quickly; retries may occur.

Fulfillment: deliver goods only when newStatus is PAID.

Webhook headers

X-Webhook-Signature: sha256=<hex HMAC-SHA256 of raw body>
X-Webhook-Timestamp: <milliseconds since epoch>
X-Webhook-Id: <unique delivery id>

Webhook body

{
  "eventType": "payment.status.changed",
  "paymentId": "550e8400-e29b-41d4-a716-446655440000",
  "merchantOrderId": "order-12345",
  "provider": "simpleswap",
  "oldStatus": "AWAITING_DEPOSIT",
  "newStatus": "PAID",
  "occurredAt": "2026-06-05T12:05:00Z",
  "metadata": { "orderId": "12345" },
  "providerDetails": {
    "providerRef": "ss-exchange-id",
    "rawStatus": "finished"
  }
}

Verify signature (PHP)

function verifyProxyWebhook(string $rawBody, string $signature, string $timestampMs, string $secret): bool
{
    $signedPayload = $timestampMs . '.' . $rawBody;
    $expected = hash_hmac('sha256', $signedPayload, $secret);
    $received = preg_replace('/^sha256=/i', '', $signature);
    return hash_equals($expected, $received);
}

Verify signature (Node.js)

import crypto from "crypto";

function verifyWebhook(body, signature, timestampMs, secret) {
  const signedPayload = `${timestampMs}.${body}`;
  const expected = crypto.createHmac("sha256", secret).update(signedPayload, "utf8").digest("hex");
  const received = signature.replace(/^sha256=/i, "");
  return crypto.timingSafeEqual(Buffer.from(received, "hex"), Buffer.from(expected, "hex"));
}

5. Customer return URL

After a terminal status, the user may land on your return_success_url or return_cancel_url with query parameters:

?payment_id=...&merchant_order_id=...&status=PAID&occurred_at=2026-06-05T12:05:00Z&sig=<hmac>

sig = HMAC_SHA256(secret, payment_id=...&merchant_order_id=...&status=...&occurred_at=...). Use for thank-you / error pages only.

6. Payment statuses

CREATED
Init OK; customer not on checkout yet
CHECKOUT
On proxy checkout; quote loaded
AWAITING_DEPOSIT
Exchange created; waiting for card payment
CONFIRMING
Payment confirming
PROCESSING
Exchange in progress
PAID
Success — deliver the product
FAILED
Payment or exchange failed
EXPIRED
Checkout session expired
CANCELLED
Cancelled by customer

7. Errors (init)

401 unauthorized — bad API key or signature

400 validation_error — missing/invalid fields

409 duplicate_order — active payment for same merchant_order_id

503 database_disabled — proxy not fully configured

Need help? Contact support with payment_id and merchant_order_id. Full markdown reference: docs/MERCHANT_API.md in the repository.