Common Issues
Authentication
401 Unauthorized on every request
Symptoms: Every API call returns 401 Unauthorized.
Causes and fixes:
-
Missing header — When sending usage to
https://api.aforo.ai/v1/ingest, send the key asX-API-Key:X-API-Key: sk_live_yourkeyOn the management APIs (
catalog.aforo.ai,pricing.aforo.ai, …) send both of these:Authorization: Bearer sk_live_yourkey X-Tenant-Id: your-tenant-id -
Sandbox vs live key — Keys are
sk_test_…(sandbox) orsk_live_…(production). Sending ask_test_key to a production endpoint (or vice-versa) returns 401. Check the prefix matches the environment you're calling. -
Key revoked — Check Developer → Developer Hub → Credentials to confirm the key is active. If revoked, create a new key.
-
Key bound to cancelled subscription — Customer API keys are automatically revoked when their subscription is cancelled. The customer must re-subscribe to get a new key.
403 Forbidden — Insufficient permissions
Your API key is valid but doesn't have permission for the action:
- MEMBER role cannot create/modify resources — use ADMIN or OWNER credentials
- BILLING_ADMIN role cannot access product catalog endpoints
- Check the endpoint's required role in the API Reference
API Requests
404 Not Found on an endpoint that exists
Symptoms: A request to a documented endpoint returns 404 Not Found, and the response detail literally contains a {...} token — for example:
{ "status": 404, "detail": "Product not found: {id}", "instance": "/api/v1/products/%7Bid%7D" }
Cause: The path still contains an unsubstituted placeholder. Endpoint paths in the reference use {id}, {customerId}, and similar tokens as placeholders — you have to replace each one with a real value before sending. Sent literally, {id} becomes part of the URL (%7Bid%7D once encoded) and matches nothing.
Fix: Substitute every {param} in the path with an actual identifier:
# Wrong — the placeholder is sent verbatim
curl https://catalog.aforo.ai/api/v1/products/{id} \
-H "Authorization: Bearer sk_live_yourkey" -H "X-Tenant-Id: your-tenant-id"
# Correct
curl https://catalog.aforo.ai/api/v1/products/prod_a1b2c3 \
-H "Authorization: Bearer sk_live_yourkey" -H "X-Tenant-Id: your-tenant-id"
If the detail echoes a real id you supplied (no braces) and you still get 404, the resource genuinely doesn't exist in your workspace — verify the id and that your X-Tenant-Id matches the workspace that owns it.
Usage Ingestion
Events not appearing in the Event Log
Check these in order:
-
Verify the endpoint — Send one event to
https://api.aforo.ai/v1/ingestor up to 1,000 tohttps://api.aforo.ai/v1/ingest/batch./v1/ingest/eventsis a different endpoint that takes the Apigee event format, not this payload. -
Validate the payload — Required fields:
{ "customerId": "required", "metricName": "required — must match an active billable unit", "quantity": 1, "occurredAt": "2026-04-17T10:00:00Z", "idempotencyKey": "required — stable across retries" } -
Check the timestamp — Events more than 90 days old are rejected with
400 Bad Request. Events more than 5 minutes in the future are rejected for clock skew. -
Run diagnostics — Call the ingestion diagnostics endpoint:
GET /v1/ingest/diagnose
Events rejected with "metric not found"
The metricName in your event payload doesn't match any active billable unit in your catalog. Check Catalog → Billable Units for the exact name.
Events rejected with an invalid executionStatus
The ingest API accepts only SUCCESS, PARTIAL, TIMEOUT, ERROR, VALIDATION_FAILED, FAILED, FAILURE, CANCELLED, PENDING, BLOCKED and HITL_REQUIRED (any case, at most 20 characters) and rejects the event for anything else. POST /v1/ingest returns 400; POST /v1/ingest/batch accepts the other events and returns 202 with the rejected one counted in failed (and, for batches of up to 100 events, listed in errors with its index and a message that includes Invalid value '…' for field 'executionStatus'). Check failed on every batch response, fix the value and resend that event with the same idempotencyKey, or leave the field off — an event without a status bills at full weight. Aforo's SDKs already drop an unknown value and log a warning.
Late arrival warnings
Events with timestamps 24+ hours in the past are flagged as late arrivals but still processed. If you're seeing many late arrival warnings, check your server clock and ensure NTP is configured correctly.
Billing & Invoices
Customer not receiving invoices
- Check the customer's email address in Commercial → Customers
- Verify your email provider is configured in Settings → Finance
- Check the billing event log for
invoice.send_failedevents - Try manually resending:
POST /api/v1/invoices/{id}/send
Invoice amount looks wrong
- Check the rate plan version pinned to the subscription — pricing changes don't apply to existing subscriptions
- Verify the billing pipeline stage breakdown in the invoice's Usage Attribution tab
- Check for applied credit notes that reduced the total
- Confirm the billing period matches expectations
Outcome-based invoice billed at full weight
The invoice line reads … 1000 billed of 1000 (…), or shows no status, although calls failed.
Rate plan preview differs from the invoice
The bill simulator and the Pricing Architect don't know your real outcome split. With no expected mix, the simulator treats every call as SUCCESS and the Architect counts every call in full and labels the estimate an upper bound; the invoice uses the actual statuses. Enter an expected mix in the simulator, or give the Architect your success and error rates, to get closer. Live revenue and Current Spend use the real statuses as events arrive, but apply included free units, block size and minimum fee per event, so they can differ from the invoice when the unit uses any of those.
Bill run partially failed
Some customers succeeded and others failed in the same bill run:
- Open the bill run detail to see per-customer results
- Failed items show error codes — common causes:
WALLET_INSUFFICIENT_FUNDS— Prepaid customer has insufficient balanceUSAGE_DATA_UNAVAILABLE— the usage store was temporarily unavailable; re-run the bill runTAX_CALCULATION_FAILED— Avalara timeout; retry the specific invoice
Subscriptions
Subscription stuck in PAST_DUE
Payment has failed and dunning is in progress:
- Check dunning attempt count:
GET /api/v1/subscriptions/{id} - Manually retry payment:
POST /api/v1/subscriptions/{id}/retry-payment - If payment method is outdated, direct customer to update it in the portal
- After resolving payment, subscription auto-moves to ACTIVE on next dunning cycle
Cannot cancel a TRIALING subscription
TRIALING subscriptions can be cancelled. If you're getting an error:
- Ensure you're calling
DELETE /api/v1/subscriptions/{id}(not/cancel) - Check for
CANCELLEDstate — it might already be cancelled
Migration fails with 409 Conflict
Common causes:
- Source subscription is already
CANCELLEDorEXPIRED— terminal states cannot be migrated - Target offering is the same as the current offering — use upgrade/downgrade endpoints instead
- A bulk migration is already in progress for this workspace — wait for it to complete
Gateway Connectivity
Integration shows ERROR status
- Go to Governance → Integrations → [your integration]
- Click Test Connection to get a specific error message
- Common fixes:
- Kong: PAT token expired — generate a new token in Kong Manager
- AWS: IAM role permissions insufficient — check the CloudFormation template was applied
- Azure: Service principal secret expired — rotate in Azure AD
- Apigee: OAuth token expired — re-authorize the integration
Products showing STALE at gateway
The gateway product config has drifted from Aforo's record:
- Click the product → Restore to Gateway
- Review the diff (what Aforo would push to the gateway)
- Confirm the restore
If the restore fails, the gateway may have additional config (plugins, routes) that need to be manually aligned.
Storefront & Domains
Subdomain shows taken, or Provision Storefront fails
Symptoms: In Channels → Domain & Routing, the subdomain field shows taken, or Provision Storefront returns a conflict.
Cause: Subdomains and custom domains are unique across all of Aforo — each handle routes to exactly one storefront, so the value you entered is already held by another organization.
Fix: Pick a different subdomain (the field re-checks availability as you type) instead of retrying the same one. Custom domains work the same way — if a hostname is already registered to another organization, choose a different one. See Domain & Routing for the full subdomain and custom-domain lifecycle.
SDK Issues
@aforoai/metering — events queued but not sending
Events are queued and one background sender ships them every 5 s or once 500 are waiting (2.0.0 defaults). During an outage — network errors, repeated 5xx/429, or a 401/403 from a wrong or revoked key — batches stay queued with their idempotency keys and the SDK logs Aforo ingestor unavailable; sending resumes on its own once the ingestor answers. If they're not sending:
- Call
await aforo.shutdown()before the process exits. The SDK installs no SIGTERM handler, so a service must call it from its own. - Check network connectivity to the ingest host, and that
baseUrlishttps://(plain http is refused except on localhost or withallowInsecureHttp: true). - For a 401/403 logged at error level, fix the API key — the queued usage is sent once a valid key is in place.
- Read
aforo.droppedCountand theonDrop(events, reason)hook:overflow(queue full),rejected(ingestor refused, e.g. unknown metric),invalid(failed a client-side check, or a middleware resolver returned no customer),shutdown(still queued whenshutdownTimeoutMsran out),closed(tracked after shutdown).
SDK middleware fails at startup: no customer resolver
From 2.0.0, every integration that meters an incoming request needs to be told who the customer is. The error reads, for example, Aforo middleware needs a customerResolver (Node), a ValueError at app start (Python FastAPI / Flask / Django), an error from metering.NewMiddleware (Go), or a Spring Boot startup failure (Java). The MCP SDKs throw from wrapToolHandler, and @aforoai/mcp-proxy exits with an error.
Cause: 2.0.0 removed the default lookup of X-Customer-Id, gRPC metadata, MCP _meta.customer_id and the MQTT username, because whoever calls your API can set them. There is no fallback to unknown or the agent id.
Fix: pass a customer resolver that returns the Aforo customer id from identity your service verified — customerResolver (Node, Go CustomerResolver), customer_resolver (Python; Django AFORO_CUSTOMER_RESOLVER), an AforoServletFilter.CustomerIdResolver bean (Java), --customer-id or --auth-tokens-file (MCP proxy). Only if a proxy you control sets the value and strips any client copy, opt in instead with trustCustomerHeader: true (Python trust_customer_header=True, Go TrustCustomerHeader: true, Java aforo.trust-customer-header: true, proxy --trust-client-customer-id). See SDKs → Who Is Billed.
SDK refuses to start: baseUrl must use https
From 2.0.0 the ingestor URL must be https://. Plain http:// is accepted only for localhost, 127.0.0.1 and ::1. For an http endpoint on a private network you trust, set allowInsecureHttp: true (Python allow_insecure_http=True, Go and Java AllowInsecureHTTP / allowInsecureHttp(true), proxy --allow-insecure-http) — the API key travels in every request.
aforo-metering (Python) — TypeError: object NoneType can't be used in 'await' expression
AforoClient.track() is synchronous: it queues the event and returns None, and a background thread sends the batch. Call it without await, including inside async def handlers:
from aforo import AforoClient
aforo = AforoClient(api_key=os.environ["AFORO_API_KEY"])
async def handler(request):
aforo.track(customer_id="cust_abc123", metric_name="api-calls", quantity=1) # no await
Call aforo.shutdown() before a short-lived script exits so the last batch is sent.
Need more help?
If you can't find an answer here, contact support and include:
- The request ID from the error response (
X-Request-Idheader) - Your workspace ID (the
X-Tenant-Idvalue) - The exact error message and HTTP status code
- Steps to reproduce