Skip to main content
This document outlines the preparatory steps required before integrating with the StablePay API, including account and credential setup, request signature algorithms, Webhook event notifications, and the headers required for each endpoint.

Essential Information

ItemValue
Production Base URLhttps://api.stablepay.co
API Prefix/api/v1
ProtocolHTTPS
Data FormatJSON
Character EncodingUTF-8

Prerequisites

Before you begin, please ensure you have:
  1. Registered for a StablePay merchant account and completed the KYB (Know Your Business) verification
  2. Created a store in the merchant dashboard, selected API as the channel type, and received approval
  3. Generated and retrieved the following from the store’s “Key Management” menu:
    • API Key: Used in the Authorization header to identify the caller
    • Secret Key: Used to generate request signatures. Store this securely and never commit it to public repositories or share it with others

Authentication & Signing

Required Headers

All API requests must include the following headers:
HeaderRequiredDescription
AuthorizationYesFormat: Bearer {api_key}
Content-TypeRequired for POST/PUTFixed value: application/json. Not required for GET requests
X-StablePay-TimestampYesUnix timestamp (seconds). Must not differ from server time by more than 5 minutes
X-StablePay-NonceYesRandom string (16–64 characters, UUID v4 recommended). Used for replay attack prevention; must be unique per request
X-StablePay-SignatureYesLowercase hexadecimal string resulting from HMAC-SHA256 of the signature payload using your Secret Key

Signature Payload Construction

sign_payload = {timestamp} + "." + {nonce} + "." + {requestBody}
  • {requestBody}: For POST/PUT requests, use the raw JSON string (do not re-serialize or prettify)
  • For GET/DELETE methods without a request body: Use an empty string for {requestBody}
  • Signature algorithm: HMAC-SHA256(sign_payload, secret_key) → lowercase hex string

Code Examples

TIMESTAMP=$(date +%s)
NONCE=$(uuidgen)
BODY='{"amount":1999,"currency":"USD"}'
SIGN_PAYLOAD="${TIMESTAMP}.${NONCE}.${BODY}"
SIGNATURE=$(echo -n "${SIGN_PAYLOAD}" | openssl dgst -sha256 -hmac "${SECRET_KEY}" | awk '{print $2}')

curl -X POST https://api.stablepay.co/api/v1/checkout/sessions/create \
  -H "Authorization: Bearer ${API_KEY}" \
  -H "X-StablePay-Timestamp: ${TIMESTAMP}" \
  -H "X-StablePay-Nonce: ${NONCE}" \
  -H "X-StablePay-Signature: ${SIGNATURE}" \
  -H "Content-Type: application/json" \
  -d "${BODY}"
The requestBody in the signature payload must be the exact raw bytes sent in the request. Do not format or reorder JSON fields after computing the signature, or the verification will fail.

Required Headers by Endpoint

CategoryExample PathAuthorizationSigning Trio
PaymentsPOST /api/v1/checkout/sessions/create
PaymentsGET /api/v1/checkout/sessions/{session_id}✅ (empty body)
PaymentsPOST /api/v1/checkout/sessions/{session_id}/cancel
RefundsPOST /api/v1/refunds/create
RefundsGET /api/v1/refunds/{refund_id}✅ (empty body)
RefundsPOST /api/v1/refunds/{refund_id}/cancel
SubscriptionsPOST /api/v1/subscriptions/create
SubscriptionsGET /api/v1/subscriptions/{subscription_id}✅ (empty body)
SubscriptionsGET /api/v1/subscriptions✅ (empty body)
SubscriptionsPOST /api/v1/subscriptions/{subscription_id}/cancel
InvoicesGET /api/v1/invoices/{invoice_id}✅ (empty body)
InvoicesGET /api/v1/invoices✅ (empty body)
Payment LookupGET /api/v1/payment/{payment_id}✅ (empty body)
Payment MethodsGET /api/v1/payment_method/{payment_method_id}✅ (empty body)
POST /api/v1/subscriptions/create also requires an additional Idempotency-Key header. When retrying with the same key, the server returns the originally created subscription object.

Idempotency

The following endpoints support idempotency using merchant-defined keys:
EndpointIdempotency Key
Create RefundRequest body field refund_id
Create SubscriptionHeader Idempotency-Key
When submitting with the same idempotency key, the server returns the originally created resource without generating duplicates.

Webhook Notifications

StablePay asynchronously pushes critical events to your configured callback URL via Webhooks.
For event examples, frozen-risk handling, and the new status = "frozen" branch under payment.failed, see Webhook Notifications.

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, e.g., payment.completed
X-StablePay-Event-IDUnique event ID for idempotent deduplication

Signature Verification Steps

  1. Extract X-StablePay-Signature, X-StablePay-Timestamp, and X-StablePay-Nonce from the request headers
  2. Verify the timestamp is within 5 minutes of the current time (replay attack prevention)
  3. Verify the nonce length is between 16 and 64 characters
  4. Take the raw request body bytes (do not parse and re-serialize), and concatenate as follows:
    sign_payload = {timestamp} + "." + {nonce} + "." + {raw_body}
    
  5. Compute HMAC-SHA256 using your Secret Key, resulting in a lowercase hex string
  6. Compare with the signature in the header using constant-time comparison (e.g., hmac.compare_digest)
import hashlib
import hmac
import time
from typing import Mapping


def verify_webhook(
    headers: Mapping[str, str],
    raw_body: bytes,
    secret_key: str,
    tolerance_seconds: int = 300,
) -> bool:
    """
    Verify StablePay webhook signature.

    Signature payload format:
        "{timestamp}.{nonce}.{raw_body}"

    Notes:
    - raw_body must be the original HTTP request body bytes.
    - do not parse JSON and serialize it again before verification.
    """

    if not secret_key:
        return False

    timestamp = headers.get("X-StablePay-Timestamp")
    nonce = headers.get("X-StablePay-Nonce")
    signature = headers.get("X-StablePay-Signature")

    if not timestamp or not nonce or not signature:
        return False

    try:
        ts = int(timestamp)
    except ValueError:
        return False

    if abs(int(time.time()) - ts) > tolerance_seconds:
        return False

    if not (16 <= len(nonce) <= 64):
        return False

    signed_payload = (
        timestamp.encode("utf-8")
        + b"."
        + nonce.encode("utf-8")
        + b"."
        + raw_body
    )

    expected_signature = hmac.new(
        secret_key.encode("utf-8"),
        signed_payload,
        hashlib.sha256,
    ).hexdigest()

    return hmac.compare_digest(expected_signature, signature)

Response Requirements & Retry Policy

  • Your service must return HTTP 2xx within 30 seconds
  • Returning 429 or 5xx will trigger a retry, while other 4xx responses will not be retried
  • Maximum of 10 retries with exponential backoff intervals (minutes): 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024

Idempotent Consumption

Use X-StablePay-Event-ID (or the id field in the request body) as the idempotency key. Check whether the event has already been processed before handling to avoid side effects from duplicate deliveries.

Error Responses

Errors are returned via HTTP status codes and JSON response bodies. The error field may be a string or a structured object:
{
  "error": {
    "type": "invalid_request_error",
    "code": "parameter_missing",
    "message": "Missing required parameter: refund_id",
    "param": "refund_id",
    "doc_url": "https://docs.stablepayfi.ai/errors#parameter_missing"
  },
  "request_id": "req_xxx",
  "timestamp": 1774924800
}
Common error codes:
HTTPCodeMeaning
400parameter_missing / invalid_request_errorMissing or malformed parameters
401invalid_api_keyInvalid API Key or signature verification failure
403Insufficient permissions to access the resource
404resource_not_foundResource does not exist
409conflictIdempotency conflict (same key with different parameters)
429DAILY_REFUND_LIMIT_EXCEEDED, etc.Rate limit or quota exceeded

Security Best Practices

  • Secret Keys should only be used on the server side and must never appear in frontend code, APKs, mini-program packages, or logs
  • We recommend injecting Secret Keys via a Key Management Service (KMS/Vault/SSM) in your CI/CD pipeline
  • Webhook callback URLs must use HTTPS
  • For every received Webhook, perform signature verification + idempotent deduplication + business state validation (e.g., only process refunds if the order status is “paid”)
  • Regularly rotate your API Keys and Secret Keys in the merchant dashboard
Last modified on May 15, 2026