Quick Start

Complete payment integration in 5 steps

1

Register and Get Your API Key

Sign up for a merchant account, create a project in the dashboard, and obtain your Access Key and Secret Key.

2

Configure Webhook URL

Set up the Webhook notification URL in your project settings. The system will push notifications when order status changes.

3

Call the Create Order API

Use HMAC-SHA256 to sign your request and call POST /payments to get a receiving address and checkout link.

4

Handle Webhook Notifications

Receive Webhook notifications, verify the X-BS-Signature, and update your local order status.

5

Test and Go Live

Complete end-to-end testing in the sandbox environment, then switch to production once verified.

🔑

HMAC-SHA256 Signature Authentication

Each request must include an Access Key, timestamp, random Nonce, and HMAC signature to ensure requests are unforgeable.

🔔

Async Notifications (Webhook)

Supports 8 event types including payment.completed, order.expired, and payment.underpaid — covering the full payment lifecycle.

🔄

12-State Order State Machine

PENDING → PAID_UNCONFIRMED → CONFIRMED → COMPLETED, with exception branches (underpaid/overpaid/expired/risk control) — clear and controllable state transitions.

🔒

Idempotency & Replay Prevention

Nonce prevents replay attacks, merchant_order_no prevents duplicate creation, event_id prevents duplicate processing — triple idempotency protection.

API Authentication

All merchant API requests require HMAC-SHA256 signature authentication

Required Headers

HeaderDescriptionExample
X-BS-Access-KeyMerchant Access Key (public key), identifies the merchantak_live_xxxxxxxxxxxx
X-BS-TimestampRequest timestamp (Unix seconds), must be within 5 minutes of server time1704067200
X-BS-NonceRandom string (UUID) for replay prevention, unique per request550e8400-e29b-41d4-a716-446655440000
X-BS-SignatureHMAC-SHA256 signature (Hex encoded)a1b2c3d4e5f6...
Content-TypeFixed as JSON formatapplication/json

Signature Algorithm

signature.txt
// 1. Construct signature body
signature_body = "{timestamp}" + "." + "{nonce}" + "." + "{request_body}"

// 2. Compute HMAC-SHA256 with Secret Key
signature = HMAC-SHA256(signature_body, secret_key)

// 3. Convert signature to Hex string, set as X-BS-Signature header
X-BS-Signature = hex(signature)

// Example:
// timestamp  = "1704067200"
// nonce      = "550e8400-e29b-41d4-a716-446655440000"
// body       = '{"merchant_order_no":"ORD-001","fiat_amount":"100.00",...}'
// secret_key = "sk_live_your_secret_key"
// signature_body = "1704067200.550e8400-e29b-41d4-a716-446655440000.{...json...}"
Security Tip:Secret Key is only shown once during creation. Store it securely. Never expose Secret Key in frontend code — all signature calculations should be performed server-side.

API Reference Documentation

Base URL: https://api.blocksettler.com/api/v1/merchant

POST/paymentsCreate Payment Order

Request Parameters (Request Body)

FieldTypeRequiredDescription
merchant_order_nostringYesMerchant order number, unique within the project (max 64 chars)
titlestringYesOrder title, displayed to the payer (max 128 chars)
descriptionstringNoOrder description (max 512 chars)
fiat_amountstringYesFiat amount, e.g. "100.00"
fiat_currencystringYesFiat currency, e.g. "USD", "CNY"
pay_chainstringNoPayment chain: POLYGON / TRON / ETH / BSC / SOLANA. Uses project default if not specified
pay_tokenstringNoPayment token: USDT / USDC. Uses project default if not specified
notify_urlstringNoAsync notification URL. Uses project default if not specified
return_urlstringNoFrontend redirect URL after payment completion
expires_inintNoOrder validity period (seconds), range 300~86400, default 1800 (30 min)
metadataobjectNoCustom merchant data, returned as-is in async notifications

Response Parameters (Response Body)

FieldTypeDescription
platform_order_nostringPlatform order number (unique ID generated by BlockSettler)
merchant_order_nostringMerchant order number (as provided in the request)
statusstringOrder status, initially "PENDING"
fiat_amountstringFiat amount
fiat_currencystringFiat currency
pay_amountstringCrypto amount to pay (calculated at real-time exchange rate)
paid_amountstringCrypto amount already paid (0 upon creation)
pay_chainstringPayment chain name
pay_tokenstringPayment token
pay_addressstringReceiving address (payer must transfer to this address)
expired_atstringOrder expiration time (ISO 8601 format)
created_atstringCreation time

Request Example

POST /api/v1/merchant/payments
// Request Body
{
  "merchant_order_no": "ORD-20250415-0001",
  "title": "Premium Plan - 1 Month",
  "description": "Premium membership subscription",
  "fiat_amount": "99.00",
  "fiat_currency": "USD",
  "pay_chain": "TRON",
  "pay_token": "USDT",
  "notify_url": "https://your-site.com/api/payment/notify",
  "return_url": "https://your-site.com/payment/success",
  "expires_in": 1800,
  "metadata": {
    "user_id": "U-10086",
    "plan": "premium"
  }
}

Response Example

Response 200 OK
{
  "code": 200,
  "message": "success",
  "data": {
    "platform_order_no": "BS2025041500000001",
    "merchant_order_no": "ORD-20250415-0001",
    "status": "PENDING",
    "fiat_amount": "99.00",
    "fiat_currency": "USD",
    "pay_amount": "99.000000000000000000",
    "paid_amount": "0.000000000000000000",
    "pay_chain": "TRON",
    "pay_token": "USDT",
    "pay_address": "TN7gK3Wfxy4RDXG5DPGnXmkJJPqozvMBkR",
    "return_url": "https://your-site.com/payment/success",
    "expired_at": "2025-04-15T11:30:00Z",
    "created_at": "2025-04-15T11:00:00Z"
  }
}
GET/payments/:order_noQuery Order Details

Query the latest order status by platform order number. :order_no = platform_order_no

Response Example (After Payment)

Response 200 OK
{
  "code": 200,
  "message": "success",
  "data": {
    "platform_order_no": "BS2025041500000001",
    "merchant_order_no": "ORD-20250415-0001",
    "status": "COMPLETED",
    "fiat_amount": "99.00",
    "fiat_currency": "USD",
    "pay_amount": "99.000000000000000000",
    "paid_amount": "99.000000000000000000",
    "pay_chain": "TRON",
    "pay_token": "USDT",
    "pay_address": "TN7gK3Wfxy4RDXG5DPGnXmkJJPqozvMBkR",
    "expired_at": "2025-04-15T11:30:00Z",
    "paid_at": "2025-04-15T11:05:32Z",
    "confirmed_at": "2025-04-15T11:06:45Z",
    "completed_at": "2025-04-15T11:06:45Z",
    "created_at": "2025-04-15T11:00:00Z"
  }
}
POST/payments/:order_no/closeClose Order

Close a pending payment order. Only orders in PENDING, UNDERPAID, OVERPAID, LATE_PAID status can be closed.

Request Parameters

FieldTypeRequiredDescription
reasonstringNoReason for closing (max 128 chars)
INFOResponse Format & Error Codes

All API endpoints return a unified JSON structure:

response_format.json
// Success response
{ "code": 200, "message": "success", "data": { ... } }

// Invalid parameters
{ "code": 400, "message": "invalid fiat_amount", "data": null }

// Authentication failed (invalid signature / Access Key / expired timestamp)
{ "code": 401, "message": "invalid signature", "data": null }

// Resource not found
{ "code": 404, "message": "order not found", "data": null }

// Internal server error
{ "code": 500, "message": "internal error", "data": null }

Async Notifications (Webhook)

When an order status changes, BlockSettler sends an HTTP POST request to the merchant's configured callback URL

Supported Event Types

event_typeTriggerDescription
payment.confirmedOn-chain confirmations reachedTransaction confirmed, safe to deliver goods/services
payment.completedOrder settlement completedOrder flow completed normally
order.expiredOrder timed out without paymentValidity period exceeded without full payment
order.closedOrder closedClosed by merchant or system
payment.underpaidInsufficient payment amountAmount received is less than required
payment.overpaidExcess payment amountAmount received exceeds required amount
payment.late_paidPayment received after expiryOrder expired but on-chain payment still received
payment.failedPayment failedRisk control review rejected or other anomaly

Notification Payload Format

The system sends a POST request to the merchant's notify_url with Content-Type application/json.

Notification Headers

HeaderDescription
Content-Typeapplication/json
X-BS-Event-IDUnique event ID for idempotent deduplication
X-BS-Event-TypeEvent type, e.g. payment.completed
X-BS-TimestampNotification timestamp (Unix seconds)
X-BS-SignatureHMAC-SHA256 signature (for verifying notification source)

Notification Body Example (payment.completed)

webhook_payload.json
{
  "event_id": "evt_8f14e45f-ceea-4a4d-a8e0-3c2b5d4e9f01",
  "event_type": "payment.completed",
  "timestamp": "2025-04-15T11:06:45Z",
  "platform_order_no": "BS2025041500000001",
  "status": "COMPLETED",
  "payment": {
    "fiat_amount": "99.00",
    "fiat_currency": "USD",
    "pay_amount": "99.000000",
    "paid_amount": "99.000000",
    "pay_chain": "TRON",
    "pay_token": "USDT",
    "pay_address": "TN7gK3Wfxy4RDXG5DPGnXmkJJPqozvMBkR",
    "tx_hash": "6a1b2c3d4e5f...abc123def456"
  }
}

Signature Verification

After receiving a notification, the merchant must verify the X-BS-Signature to confirm the notification source is trusted. Verification method:

verify_signature.txt
// 1. Get timestamp and signature from request headers
timestamp  = request.headers["X-BS-Timestamp"]
signature  = request.headers["X-BS-Signature"]

// 2. Read the raw request body (JSON string)
raw_body   = request.body

// 3. Construct signature body and compute HMAC-SHA256
expected   = HMAC-SHA256(timestamp + "." + raw_body, your_webhook_secret)

// 4. Use constant-time comparison to verify signature (prevent timing attacks)
is_valid   = constant_time_equal(hex(expected), signature)

Retry Mechanism

Success Criteria

Merchant endpoint returns HTTP 2xx status code

Retry Strategy

Exponential backoff: 1s → 5s → 30s → 5m → 30m → 2h, max 6 retries

Idempotent Processing

Use event_id for idempotent checks — the same event may be delivered multiple times

Best Practice:Return 200 OK immediately upon receiving a notification, then process business logic asynchronously. Lengthy processing (>5s) may cause timeouts and trigger retries.

Code Samples

Complete examples for creating orders (with signing) and receiving Webhook notifications

create_order.sh
# ====== Create Payment Order ======

TIMESTAMP=$(date +%s)
NONCE=$(uuidgen)
BODY='{"merchant_order_no":"ORD-20250415-0001","title":"Premium Plan","fiat_amount":"99.00","fiat_currency":"USD","pay_chain":"TRON","pay_token":"USDT","notify_url":"https://your-site.com/api/notify"}'

# Compute signature: HMAC-SHA256(timestamp.nonce.body, secret_key)
SIGN_BODY="${TIMESTAMP}.${NONCE}.${BODY}"
SIGNATURE=$(echo -n "${SIGN_BODY}" | openssl dgst -sha256 -hmac "sk_live_xxxx" | awk '{print $2}')

curl -X POST https://api.blocksettler.com/api/v1/merchant/payments \
  -H "Content-Type: application/json" \
  -H "X-BS-Access-Key: ak_live_xxxxxxxxxxxx" \
  -H "X-BS-Timestamp: ${TIMESTAMP}" \
  -H "X-BS-Nonce: ${NONCE}" \
  -H "X-BS-Signature: ${SIGNATURE}" \
  -d "${BODY}"

# ====== Query Order ======
curl -X GET https://api.blocksettler.com/api/v1/merchant/payments/BS2025041500000001 \
  -H "X-BS-Access-Key: ak_live_xxxxxxxxxxxx" \
  -H "X-BS-Timestamp: ${TIMESTAMP}" \
  -H "X-BS-Nonce: $(uuidgen)" \
  -H "X-BS-Signature: {computed_signature}"

Order Status Transitions

BlockSettler uses a 12-state machine to manage the order lifecycle

CREATEDCreated
PENDINGPending Payment
PAID_UNCONFIRMEDPaid, Unconfirmed
CONFIRMEDConfirmed
COMPLETEDCompleted
EXPIREDExpired
CLOSEDClosed
UNDERPAIDUnderpaid
OVERPAIDOverpaid
LATE_PAIDLate Payment
RISK_REVIEWRisk Review
FAILEDFailed
order_status_flow.txt
                         Normal Payment Flow
CREATED ──> PENDING ──> PAID_UNCONFIRMED ──> CONFIRMED ──> COMPLETED
                │
                │          Exception Branches
                ├──> EXPIRED          Timed out without payment
                ├──> CLOSED           Closed by merchant
                ├──> UNDERPAID        Paid < Required
                ├──> OVERPAID         Paid > Required
                └──> LATE_PAID        Payment after expiry

EXPIRED ──> LATE_PAID                Payment received after expiry
PAID_UNCONFIRMED ──> RISK_REVIEW   Risk control triggered
RISK_REVIEW ──> CONFIRMED / FAILED  Approved / Rejected

Ready to Start Integrating?

Register for an API Key, test in the sandbox environment, and go live in 1 day on average