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 prefix | Correct 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, notpass_installedorPassInstalled). - 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 Bufferconst 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 differencesconst 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:
-
Wrong base URL — using
api.qairopay.comwith aqp_test_key, orsandbox-api.qairopay.comwith aqp_live_key. Both return401 Unauthorized. Check the key prefix and base URL match. -
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:
- Identify the cause — check your server logs for the period when deliveries started failing. Common causes: TLS certificate expired, server downtime, handler returning 5xx.
- Fix the underlying issue before re-enabling. Re-enabling a broken endpoint just restarts the failure cycle.
- Re-enable the endpoint:
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"}'- 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.