Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.stablepayfi.ai/llms.txt

Use this file to discover all available pages before exploring further.

StablePay uses Webhooks to asynchronously push key payment, refund, subscription, and invoice events to the callback URL configured in your merchant dashboard. This page summarizes Webhook event types, request headers, signature verification requirements, and the recent risk-control update that introduces the frozen payment status.

Event types

CategoryEvent types
Paymentspayment.completed, payment.failed, payment.expired, payment.cancelled
Refundsrefund.succeeded, refund.failed
Subscriptionssubscription.created, subscription.trialing, subscription.active, subscription.past_due, subscription.canceled, subscription.updated, subscription.incomplete_expired
Invoicesinvoice.created, invoice.paid, invoice.payment_failed

Webhook headers

HeaderDescription
Content-TypeFixed value: application/json
User-AgentStablePay-Webhook/1.0
X-StablePay-SignatureLowercase hex signature of {timestamp}.{nonce}.{raw_body} using HMAC-SHA256
X-StablePay-TimestampUnix timestamp (seconds)
X-StablePay-NonceRandom string (16–64 characters), included in signature calculation, unique per notification
X-StablePay-Event-TypeEvent type, for example payment.completed
X-StablePay-Event-IDUnique event ID used for idempotent deduplication

Signature verification

Always verify the signature using the original request body bytes. Do not parse the JSON and serialize it again before verification. Signature payload format:
sign_payload = {timestamp} + "." + {nonce} + "." + {raw_body}
Recommended verification flow:
  1. Read X-StablePay-Signature, X-StablePay-Timestamp, and X-StablePay-Nonce
  2. Verify the timestamp is within 5 minutes of current time
  3. Verify the nonce length is between 16 and 64 characters
  4. Compute HMAC-SHA256 using your Secret Key
  5. Compare signatures using a constant-time comparison method
For code examples, see Pre-Integration Setup.

Response and retry behavior

  • Your service should return HTTP 2xx within 30 seconds
  • Returning 429 or 5xx enters the retry queue; other 4xx responses are not retried by default
  • StablePay retries up to 10 times
  • Use X-StablePay-Event-ID or the id field in the request body as your idempotency key

payment.failed and the new frozen status

StablePay recently strengthened its risk-control capabilities. After this rollout, when a payment order triggers a high-risk rule, the funds may be temporarily frozen. These cases typically involve suspected illicit activity, abnormal transaction behavior, or other high-risk patterns, and require additional review before the final outcome is determined. After this change goes live, StablePay will continue using the existing payment.failed Webhook event type. However, the payment status in the callback can now include:
"status": "frozen"
This means your payment.failed handler should now distinguish at least two cases in data.object.status:
  • failed: handle as a standard payment failure
  • frozen: do not treat as a standard payment failure
Depending on the channel or integration source, extended business fields may appear inside metadata or as standalone fields. Use type, id, and the core state fields inside data.object as the primary source of truth for your logic.

Example frozen callback

{
  "id": "evt_1778818565572372433",
  "data": {
    "object": {
      "amount": 1,
      "status": "frozen",
      "currency": "USDT",
      "metadata": {
        "source": "api",
        "wc_order_id": "order_0b543c39"
      },
      "order_id": "order_0b543c39",
      "session_id": "20260515110100101649130000272078",
      "exchange_rate": ""
    }
  },
  "type": "payment.failed",
  "created_at": 1778818549
}

Handling guidance

When you receive payment.failed, add an extra check for data.object.status:
  • If status = "failed", keep your existing payment-failure handling
  • If status = "frozen", mark the order as something like risk_frozen or pending_risk_review
For frozen cases, do not automatically treat the order as a normal failed payment, and avoid actions such as:
  • automatic make-good / replacement fulfillment
  • automatic shipment
  • automatic entitlement release or service activation

Event examples

payment.completed

{
  "id": "evt_1778835561972546443",
  "type": "payment.completed",
  "created_at": 1778835561,
  "data": {
    "object": {
      "amount": 1,
      "status": "completed",
      "currency": "USDT",
      "order_id": "order_56929d9f",
      "session_id": "20260515110100101170210000082012",
      "exchange_rate": "",
      "source": "api"
    }
  }
}

payment.failed

{
  "id": "evt_1778834854265555041",
  "type": "payment.failed",
  "created_at": 1778834851,
  "data": {
    "object": {
      "amount": 1,
      "status": "frozen",
      "currency": "USDT",
      "order_id": "order_31509b43",
      "session_id": "20260515110100101170210000082010",
      "exchange_rate": "",
      "source": "api"
    }
  }
}

payment.expired

{
  "id": "evt_1778836650899324281",
  "type": "payment.expired",
  "created_at": 1778836650,
  "data": {
    "object": {
      "amount": 0,
      "status": "expired",
      "currency": "",
      "order_id": "order_d3ff11a8",
      "session_id": "20260515110100101170210000082011",
      "exchange_rate": "",
      "source": "api"
    }
  }
}

payment.cancelled

{
  "id": "evt_1770864227443530013",
  "type": "payment.cancelled",
  "created_at": 1770864227,
  "data": {
    "object": {
      "amount": 0,
      "status": "canceled",
      "currency": "",
      "order_id": "order_29",
      "session_id": "20260212110100101170210000028002",
      "exchange_rate": "",
      "source": "api"
    }
  }
}
Last modified on May 15, 2026