Skip to content
Ask the docs

Find answers across the QairoPay docs.

Type a question and we'll synthesize an answer from the docs with citations back to the source pages.

Troubleshooting

Quick reference for the most common integration issues.

401 Unauthorized

Cause: The most common cause is using a test key (qp_test_) against the production base URL, or a live key (qp_live_) against the sandbox URL.

Key prefixCorrect base URL
qp_test_https://sandbox-api.qairopay.com
qp_live_https://api.qairopay.com

The SDK sets the base URL automatically from the key prefix. If you’re constructing requests manually, verify the URL matches your key.

Other causes:

  • The key has been revoked — create a new one in Developer → API Keys.
  • The key is missing from the request — verify Authorization: Bearer <key> is present on every call.
  • A typo or extra whitespace in the key value.

422 Unprocessable Entity

Cause: A field failed server-side validation.

The error response includes a details array identifying each failing field:

{
"error": {
"type": "invalid_request",
"code": "validation_failed",
"message": "Request body failed validation",
"details": [
{ "field": "enabled_events[0]", "issue": "unknown event type 'pass.suspended'" },
{ "field": "url", "issue": "must be HTTPS in the live environment" }
]
}
}

Common field format requirements:

  • Event names are dot-separated (pass.installed, not pass_installed or PassInstalled).
  • URLs for webhook endpoints must be HTTPS in the live environment.
  • Monetary amounts are in cents (integer), not decimal dollars.
  • IDs use the resource prefix format (tpl_…, pss_…, whe_…).

Webhook signature verification failure

Three causes account for nearly all verification failures:

1. Wrong message format. The signed message is <unix_timestamp>.<raw_body_bytes> — not <timestamp>.<json_string>. If you’re reconstructing the signed message manually, use the raw request body bytes before any JSON parsing.

2. Encoding mismatch. Pass rawBody (a Buffer or bytes), not JSON.stringify(req.body). Parsing and re-serializing JSON can change whitespace, key order, or number representation, which invalidates the signature.

// ✅ Correct — pass raw Buffer
const event = await QairoPay.webhooks.constructEvent(
req.body, // express.raw() middleware gives you this as a Buffer
req.headers['QairoPay-Signature'],
process.env.QAIROPAY_WEBHOOK_SECRET!,
);
// ❌ Wrong — JSON.stringify introduces encoding differences
const event = await QairoPay.webhooks.constructEvent(
Buffer.from(JSON.stringify(req.body)),
...
);

3. Clock skew greater than 300 seconds. Events with a timestamp more than 5 minutes old are rejected to prevent replay attacks. Sync your server clock with NTP.

429 Too Many Requests

Cause: You’ve exceeded the rate limit for your plan tier.

The response includes Retry-After (seconds to wait) and X-RateLimit-Remaining (requests left in the current window).

Back-off pattern:

async function withRetry<T>(fn: () => Promise<T>): Promise<T> {
try {
return await fn();
} catch (err) {
if (err instanceof RateLimitError) {
await sleep(err.retryAfterMs ?? 1000);
return fn();
}
throw err;
}
}

For sustained high-volume workloads, consider batching or queuing requests to smooth out bursts. See Rate limits for per-tier limits.

Sandbox vs live confusion

The two most common mistakes when switching environments:

  1. Wrong base URL — using api.qairopay.com with a qp_test_ key, or sandbox-api.qairopay.com with a qp_live_ key. Both return 401 Unauthorized. Check the key prefix and base URL match.

  2. Wrong key in the wrong service — a deployment pipeline accidentally shipped the sandbox key to the production environment, or vice versa. Add an assertion at startup that the key prefix matches the expected environment:

const key = process.env.QAIROPAY_API_KEY!;
if (process.env.NODE_ENV === "production" && !key.startsWith("qp_live_")) {
throw new Error("Production build must use a live API key");
}

Endpoint auto-disable recovery

When an endpoint is auto-disabled after 5 consecutive delivery failures, you’ll see status: "disabled" and disabled_reason: "retry_exhausted" in the dashboard or API response.

Steps to recover:

  1. Identify the cause — check your server logs for the period when deliveries started failing. Common causes: TLS certificate expired, server downtime, handler returning 5xx.
  2. Fix the underlying issue before re-enabling. Re-enabling a broken endpoint just restarts the failure cycle.
  3. Re-enable the endpoint:
Terminal window
curl -X PATCH https://api.qairopay.com/v1/webhook-endpoints/whe_01HABC \
-H "Authorization: Bearer $QAIROPAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"status": "enabled"}'
  1. Send a test delivery from the dashboard to confirm the endpoint is receiving events.

Note: events are not automatically replayed after re-enable. Replay missed deliveries manually from the dashboard if needed.