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.
| Item | Value |
|---|
| Production Base URL | https://api.stablepay.co |
| API Prefix | /api/v1 |
| Protocol | HTTPS |
| Data Format | JSON |
| Character Encoding | UTF-8 |
Prerequisites
Before you begin, please ensure you have:
- Registered for a StablePay merchant account and completed the KYB (Know Your Business) verification
- Created a store in the merchant dashboard, selected API as the channel type, and received approval
- 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
All API requests must include the following headers:
| Header | Required | Description |
|---|
Authorization | Yes | Format: Bearer {api_key} |
Content-Type | Required for POST/PUT | Fixed value: application/json. Not required for GET requests |
X-StablePay-Timestamp | Yes | Unix timestamp (seconds). Must not differ from server time by more than 5 minutes |
X-StablePay-Nonce | Yes | Random string (16–64 characters, UUID v4 recommended). Used for replay attack prevention; must be unique per request |
X-StablePay-Signature | Yes | Lowercase 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.
| Category | Example Path | Authorization | Signing Trio |
|---|
| Payments | POST /api/v1/checkout/sessions/create | ✅ | ✅ |
| Payments | GET /api/v1/checkout/sessions/{session_id} | ✅ | ✅ (empty body) |
| Payments | POST /api/v1/checkout/sessions/{session_id}/cancel | ✅ | ✅ |
| Refunds | POST /api/v1/refunds/create | ✅ | ✅ |
| Refunds | GET /api/v1/refunds/{refund_id} | ✅ | ✅ (empty body) |
| Refunds | POST /api/v1/refunds/{refund_id}/cancel | ✅ | ✅ |
| Subscriptions | POST /api/v1/subscriptions/create | ✅ | ✅ |
| Subscriptions | GET /api/v1/subscriptions/{subscription_id} | ✅ | ✅ (empty body) |
| Subscriptions | GET /api/v1/subscriptions | ✅ | ✅ (empty body) |
| Subscriptions | POST /api/v1/subscriptions/{subscription_id}/cancel | ✅ | ✅ |
| Invoices | GET /api/v1/invoices/{invoice_id} | ✅ | ✅ (empty body) |
| Invoices | GET /api/v1/invoices | ✅ | ✅ (empty body) |
| Payment Lookup | GET /api/v1/payment/{payment_id} | ✅ | ✅ (empty body) |
| Payment Methods | GET /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:
| Endpoint | Idempotency Key |
|---|
| Create Refund | Request body field refund_id |
| Create Subscription | Header 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
| Category | Event Types |
|---|
| Payments | payment.completed, payment.failed, payment.expired, payment.cancelled |
| Refunds | refund.succeeded, refund.failed |
| Subscriptions | subscription.created, subscription.trialing, subscription.active, subscription.past_due, subscription.canceled, subscription.updated, subscription.incomplete_expired |
| Invoices | invoice.created, invoice.paid, invoice.payment_failed |
| Header | Description |
|---|
Content-Type | Fixed value: application/json |
User-Agent | StablePay-Webhook/1.0 |
X-StablePay-Signature | Lowercase hex signature of {timestamp}.{nonce}.{raw_body} using HMAC-SHA256 |
X-StablePay-Timestamp | Unix timestamp (seconds) |
X-StablePay-Nonce | Random string (16–64 characters), included in signature calculation, unique per notification |
X-StablePay-Event-Type | Event type, e.g., payment.completed |
X-StablePay-Event-ID | Unique event ID for idempotent deduplication |
Signature Verification Steps
-
Extract
X-StablePay-Signature, X-StablePay-Timestamp, and X-StablePay-Nonce from the request headers
-
Verify the timestamp is within 5 minutes of the current time (replay attack prevention)
-
Verify the nonce length is between 16 and 64 characters
-
Take the raw request body bytes (do not parse and re-serialize), and concatenate as follows:
sign_payload = {timestamp} + "." + {nonce} + "." + {raw_body}
-
Compute HMAC-SHA256 using your Secret Key, resulting in a lowercase hex string
-
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:
| HTTP | Code | Meaning |
|---|
| 400 | parameter_missing / invalid_request_error | Missing or malformed parameters |
| 401 | invalid_api_key | Invalid API Key or signature verification failure |
| 403 | — | Insufficient permissions to access the resource |
| 404 | resource_not_found | Resource does not exist |
| 409 | conflict | Idempotency conflict (same key with different parameters) |
| 429 | DAILY_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