API Error Reference
This page covers the most common HTTP errors returned by the NAT API and how to resolve them.
401 Unauthorized — invalid or missing API key
Error body:
{
"error": "INVALID_API_KEY",
"message": "The API key provided is invalid or has been revoked.",
"status": 401
}Causes and fixes:
| Cause | Fix |
|---|---|
Wrong header name (Authorization: Bearer instead of X-API-Key) | Use X-API-Key: nat_pk_... |
| Trailing whitespace or truncated key | Copy the full key from the dashboard |
| Key revoked after rotation | Use the new key from Settings → API Keys |
| Key not yet provisioned (new signup) | Wait a few seconds, then retry |
Verify your key with a quick health check:
curl https://api.nat-testing.io/api/v1/usage \
-H "X-API-Key: $NAT_API_KEY"A 200 OK response confirms the key is valid. See Onboarding Issues for more detail.
402 Payment Required — quota exceeded
Error body:
{
"error": "QUOTA_EXCEEDED",
"message": "Monthly scan quota exhausted. Quota resets on 2025-02-01.",
"quota_reset_date": "2025-02-01T00:00:00Z"
}This error is returned when you attempt to start a new scan after exhausting your monthly quota. In-flight scans are not interrupted.
Resolution:
- Wait for automatic reset — quota resets on
quota_reset_date(first day of the next billing period) - Check remaining quota —
GET /api/v1/usagereturnsscans_remainingandquota_reset_date - Upgrade your plan — immediately increase your quota:
curl -X POST https://api.nat-testing.io/api/v1/billing/checkout \ -H "X-API-Key: $NAT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "price_id": "price_pro_monthly", "success_url": "https://app.nat-testing.io/dashboard?checkout=success", "cancel_url": "https://app.nat-testing.io/dashboard" }'
See Billing & Plans for plan quotas and a comparison table.
429 Too Many Requests — rate limited
Error body:
{
"error": "RATE_LIMITED",
"message": "Too many requests. Please slow down and retry after 10 seconds.",
"retry_after": 10
}NAT enforces per-tenant rate limits to ensure fair use. The retry_after field (in seconds) indicates when you can safely retry.
Recommended retry pattern:
import time
import requests
def call_with_retry(url, headers, **kwargs):
while True:
response = requests.get(url, headers=headers, **kwargs)
if response.status_code == 429:
retry_after = int(response.json().get("retry_after", 10))
print(f"Rate limited. Retrying in {retry_after}s...")
time.sleep(retry_after)
continue
response.raise_for_status()
return responseasync function callWithRetry(url, options) {
while (true) {
const res = await fetch(url, options);
if (res.status === 429) {
const { retry_after = 10 } = await res.json();
console.log(`Rate limited. Retrying in ${retry_after}s...`);
await new Promise((r) => setTimeout(r, retry_after * 1000));
continue;
}
return res;
}
}If you are running scans in a tight loop from CI/CD, add a short delay between scan submissions (time.sleep(1) or equivalent) to avoid triggering rate limits.
503 Service Unavailable — SYNC_NOT_CONFIGURED
Error body:
{
"error": "SYNC_NOT_CONFIGURED",
"message": "NAT_APP_SYNC_KEY is not configured. Set this environment variable to enable tenant synchronization.",
"status": 503
}Cause: The NAT_APP_SYNC_KEY environment variable is not set on the NAT engine server. This variable is required for tenant synchronization between the NAT app and the engine.
Fix (self-hosted deployments):
- Obtain the sync key from your NAT app admin dashboard or from your deployment configuration
- Set the environment variable:
Or in your Docker Compose file:
export NAT_APP_SYNC_KEY="your_sync_key_here"environment: - NAT_APP_SYNC_KEY=your_sync_key_here - Restart the NAT engine server
NAT_APP_SYNC_KEY is a server-side secret — never expose it in client-side code or version control.
For a full list of required environment variables, see the Self-Hosted Setup guide.
Webhook signature verification failures — 400 Bad Request
Symptom: Stripe webhooks return 400 Bad Request and events are not processed.
Cause: The Stripe-Signature header does not match the expected signature computed from STRIPE_WEBHOOK_SECRET.
Diagnosis steps:
- In the Stripe dashboard, go to Developers → Webhooks and click your endpoint
- Check the delivery log for
400responses and review the error detail - Verify the
STRIPE_WEBHOOK_SECRETvalue matches exactly:# The secret starts with whsec_ echo $STRIPE_WEBHOOK_SECRET - For local development, use the Stripe CLI to forward webhooks (it handles signing automatically):
stripe listen --forward-to localhost:8080/api/v1/billing/webhook
Common mistakes:
| Mistake | Fix |
|---|---|
| Using the API key instead of the webhook signing secret | Copy whsec_... from Stripe webhook endpoint page, not from API keys |
| Middleware consuming the raw request body before signature check | Ensure the webhook route reads the raw body before any body-parsing middleware |
| Clock skew greater than 5 minutes | Sync server time with NTP — Stripe rejects requests with timestamps >5 min old |
See Billing Issues for more webhook debugging steps.
Related pages
- Onboarding Issues — first-time setup problems
- Auth Issues — authentication failures when scanning
- Billing Issues — subscription and quota problems
- Billing API — checkout, portal, and webhook endpoints
- Usage & Quotas API — monitor consumption and alerts