Error Codes
This reference documents every error code returned by the ItPay Protocol API. Understanding these codes — their HTTP status, meaning, and retry semantics — is essential for building robust payment integrations.
1. Error Response Format
All API errors follow a consistent JSON structure. Every error response includes a code, a message, and optionally a details object with additional context.
Standard Shape
{
"error": {
"code": "SERVICE_NOT_FOUND",
"message": "The requested service was not found. Verify the service_id and try again.",
"details": {
"service_id": "svc_invalid_999",
"reason": "no_manifest_registered"
}
},
"request_id": "req_abc123def456"
}
| Field | Type | Description |
|---|---|---|
error.code | string | Machine-readable error code. Use this for programmatic handling. |
error.message | string | Human-readable description of the error. Suitable for logging or display. |
error.details | object | (Optional) Additional context — field names, limits exceeded, resource IDs, etc. |
request_id | string | Unique identifier for the request. Include this when contacting support. |
Example: Validation Error with Details
{
"error": {
"code": "AMOUNT_EXCEEDED",
"message": "The requested amount exceeds the maximum allowed for this service.",
"details": {
"requested": "5000.00",
"currency": "USD",
"max_allowed": "1000.00",
"service_id": "svc_weather_001"
}
},
"request_id": "req_xyz789ghi012"
}
2. Full Error Code Table
| Code | HTTP Status | Description | Retryable |
|---|---|---|---|
SERVICE_NOT_FOUND | 404 | The requested service_id does not have a registered manifest. Check that the service was discovered and installed before calling. | No |
INSTALL_REQUIRED | 403 | The caller has not installed the target service. The agent must complete the install flow before making payment requests. | No |
PAYMENT_REQUIRED | 402 | Payment is required to access the requested resource. Use the x402 flow — present the QR or auto-pay — then retry with a X-Payment-Proof header. | Yes |
PAYMENT_EXPIRED | 410 | The payment intent expired before settlement. The payer must initiate a new payment flow. | No — create a new PaymentIntent |
PAYMENT_CANCELLED | 400 | The payment was cancelled by the payer or the gateway before completion. | No — create a new PaymentIntent |
INSUFFICIENT_KYC | 403 | The agent or payer does not meet the KYC/KYB requirements for the requested amount or capability. Upgrade your KYC level to proceed. | No |
CHANNEL_UNAVAILABLE | 503 | The requested payment channel is temporarily unavailable (e.g., provider outage, maintenance). Switch to an alternative channel or retry after a delay. | Yes — with backoff |
AMOUNT_EXCEEDED | 400 | The requested amount exceeds the maximum per-transaction or daily limit for the service. Reduce the amount or wait for the limit window to reset. | No |
CURRENCY_UNSUPPORTED | 400 | The requested currency is not supported by the service or the selected payment channel. Check the service manifest's supported currencies. | No |
DUPLICATE_REQUEST | 409 | A request with the same idempotency key has already been processed. This is informational — the original response is returned. | No (informational only) |
SIGNATURE_INVALID | 401 | The request signature could not be verified. Check your API key, signing algorithm, and timestamp window. | No — fix signature and retry |
REFUND_NOT_ALLOWED | 400 | A refund was requested for a payment that does not support refunds, or the refund window has passed. | No |
SUBSCRIPTION_INACTIVE | 400 | The subscription is not in an active state. Check that it was successfully activated and has not expired or been cancelled. | No |
Channel-Specific Error Codes
Errors from the downstream payment channel (WeChat Pay, Alipay, etc.) are wrapped in ItPay error codes with the original channel error in details.channel_code.
| Code | HTTP Status | Description | Retryable |
|---|---|---|---|
CHANNEL_UNAVAILABLE | 503 | The payment channel is temporarily unavailable (outage, maintenance, rate limit). Switch to an alternative channel or retry with backoff. | Yes — with backoff |
CHANNEL_DOWNSTREAM_ERROR | 502 | The payment channel returned an unexpected error. See details.channel_code and details.channel_message for the original provider error. | Yes |
CHANNEL_MERCHANT_INVALID | 400 | The merchant's channel credentials (mchid, app_id, API certificate) are invalid or expired. Check the Channel Adapter configuration. | No — fix credentials |
CHANNEL_AUTH_EXPIRED | 401 | The channel authorization has expired (e.g., Alipay app_auth_token expired, WeChat API certificate expired). Re-authorize via the channel's ISV/服务商 flow. | No — re-authorize |
CHANNEL_QR_EXPIRED | 410 | The QR code's validity period ended before the user completed payment. Create a new PaymentIntent. | No — create new intent |
CHANNEL_REFUND_REJECTED | 400 | The channel rejected the refund. See details.channel_reason for provider-specific reason (e.g., WeChat: max 50 partial refunds exceeded, Alipay: refund window expired). | No |
CHANNEL_RATE_LIMITED | 429 | The downstream channel rate limited the request. Retry with the Retry-After header value. | Yes — with retry-after |
Example response:
{
"error": {
"code": "CHANNEL_MERCHANT_INVALID",
"message": "WeChat Pay merchant credentials are invalid. Verify mchid, API certificate, and APIv3 key.",
"details": {
"channel": "wechat",
"channel_code": "PARAM_ERROR",
"channel_message": "mchid and appid do not match",
"recommendation": "Check that the merchant's appid is correctly bound to mchid in the WeChat Pay merchant platform."
}
},
"request_id": "req_abc123def456"
}
3. HTTP Status Code Summary
| Status Code | Meaning | Typical Causes |
|---|---|---|
| 400 — Bad Request | The request is malformed or violates business rules. | PAYMENT_CANCELLED, AMOUNT_EXCEEDED, CURRENCY_UNSUPPORTED, REFUND_NOT_ALLOWED, SUBSCRIPTION_INACTIVE |
| 401 — Unauthorized | Authentication failed. | SIGNATURE_INVALID |
| 402 — Payment Required | Payment is needed before the resource can be served. | PAYMENT_REQUIRED |
| 403 — Forbidden | The caller lacks permission or KYC level. | INSTALL_REQUIRED, INSUFFICIENT_KYC |
| 404 — Not Found | The requested resource does not exist. | SERVICE_NOT_FOUND |
| 409 — Conflict | The request conflicts with current state. | DUPLICATE_REQUEST |
| 410 — Gone | The resource or payment intent is no longer available. | PAYMENT_EXPIRED |
| 429 — Too Many Requests | Rate limit exceeded. | Rate limiting (see rate limit headers for retry-after) |
| 503 — Service Unavailable | A downstream dependency is unavailable. | CHANNEL_UNAVAILABLE |
4. Error Handling Best Practices
Retry Strategies
Not all errors should be retried. The table above marks each error as retryable or non-retryable. Follow these guidelines:
Non-retryable errors (400, 401, 403, 404, 409, 410):
- These indicate a client-side issue or terminal state.
- Inspect the
error.codeanderror.detailsto determine the root cause. - Fix the underlying issue (wrong service ID, expired intent, missing install, invalid signature) before retrying.
Retryable errors (402, 503):
PAYMENT_REQUIRED(402): Complete the payment flow (auto-pay or QR checkout), then retry the original request with theX-Payment-Proofheader.CHANNEL_UNAVAILABLE(503): Implement exponential backoff with jitter:
Retry 1: wait 1s
Retry 2: wait 2s
Retry 3: wait 4s
Retry 4: wait 8s
Max retries: 5
Always cap retries to prevent runaway loops. Use jitter to avoid thundering herd:
function backoff(attempt: number, baseMs: number = 1000): number {
const exponential = Math.min(baseMs * Math.pow(2, attempt), 30000);
const jitter = Math.random() * 1000;
return exponential + jitter;
}
HTTP 429 (Rate Limited):
- Read the
Retry-Afterheader. If present, wait exactly that many seconds. - If no
Retry-Afterheader is present, use the same exponential backoff as 503.
Logging
Log every error response you receive. At minimum, capture:
request_id— enables support team to trace the full request lifecycleerror.code— for automated alerting and dashboardserror.message— for human-readable debuggingerror.details— structured context for root cause analysis- HTTP status code and response headers
import logging
logger = logging.getLogger("itpay")
def handle_api_error(response, body):
logger.error(
"ItPay API error",
extra={
"request_id": body.get("request_id"),
"error_code": body.get("error", {}).get("code"),
"error_message": body.get("error", {}).get("message"),
"details": body.get("error", {}).get("details"),
"http_status": response.status,
},
)
Set up alerts on error codes that indicate operational issues:
- P0 alert:
CHANNEL_UNAVAILABLE(payment channel outage affecting all users) - P1 alert:
SIGNATURE_INVALIDspikes (possible credential issue or misconfiguration) - P2 alert:
AMOUNT_EXCEEDEDorCURRENCY_UNSUPPORTED(integration configuration drift)
Webhook Error Recovery
ItPay webhooks deliver asynchronous updates about payment intents, subscriptions, and refunds. If your webhook endpoint returns a non-2xx status, ItPay retries the delivery:
| Attempt | Delay After Previous |
|---|---|
| 1st retry | 10 seconds |
| 2nd retry | 1 minute |
| 3rd retry | 10 minutes |
| 4th retry | 1 hour |
| 5th retry | 6 hours |
| 6th retry | 24 hours |
After the 6th retry failure, the webhook is permanently dropped. ItPay sends a webhook.dropped notification event via the API so you can audit missed deliveries.
Best practices for webhook recovery:
-
Idempotent handlers — Your webhook endpoint must handle duplicate deliveries safely. Use the
payment_intent_idorevent_idas an idempotency key. ItPay guarantees at-most-once processing per uniqueevent_id. -
Return 2xx quickly — Acknowledge receipt immediately and process the event asynchronously if needed. Return
200 OKwithin 5 seconds or ItPay marks the delivery as failed. -
Verify signatures — Always verify the
X-ItPay-Signatureheader before acting on a webhook payload (see Security Model). -
Poll as fallback — For critical events, implement a periodic reconciliation poll using the List Payments or Get Payment Intent endpoints to catch any webhooks that were dropped.
// Webhook handler — idempotent, verified, async
app.post("/webhooks/itpay", async (req, res) => {
// 1. Verify signature
const isValid = verifyWebhookSignature(
JSON.stringify(req.body),
req.headers["x-itpay-signature"],
process.env.ITPAY_WEBHOOK_SECRET
);
if (!isValid) return res.status(401).send("Invalid signature");
const event = req.body;
const eventId = event.id;
// 2. Idempotency check
const alreadyProcessed = await redis.exists(`webhook:${eventId}`);
if (alreadyProcessed) return res.status(200).send("OK");
// 3. Acknowledge immediately
res.status(200).send("OK");
// 4. Process asynchronously
await processEvent(event);
// 5. Mark processed
await redis.set(`webhook:${eventId}`, "1", "EX", 86400);
});
Related
- x402 HTTP 402 Semantics — The payment flow that produces
PAYMENT_REQUIREDerrors - Security Model — Signature verification, webhook validation, and anti-replay protection
- KYC/KYB — KYC level requirements that trigger
INSUFFICIENT_KYCerrors - Service Manifest — Service configuration that determines available currencies and limits