Sign in →

Common Issues

Authentication

401 Unauthorized on every request

Symptoms: Every API call returns 401 Unauthorized.

Causes and fixes:

  1. Missing header — When sending usage to https://api.aforo.ai/v1/ingest, send the key as X-API-Key:

    X-API-Key: sk_live_yourkey
    

    On the management APIs (catalog.aforo.ai, pricing.aforo.ai, …) send both of these:

    Authorization: Bearer sk_live_yourkey
    X-Tenant-Id: your-tenant-id
    
  2. Sandbox vs live key — Keys are sk_test_… (sandbox) or sk_live_… (production). Sending a sk_test_ key to a production endpoint (or vice-versa) returns 401. Check the prefix matches the environment you're calling.

  3. Key revoked — Check Developer → Developer Hub → Credentials to confirm the key is active. If revoked, create a new key.

  4. 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:

  1. Verify the endpoint — Send one event to https://api.aforo.ai/v1/ingest or up to 1,000 to https://api.aforo.ai/v1/ingest/batch. /v1/ingest/events is a different endpoint that takes the Apigee event format, not this payload.

  2. 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"
    }
    
  3. 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.

  4. 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

  1. Check the customer's email address in Commercial → Customers
  2. Verify your email provider is configured in Settings → Finance
  3. Check the billing event log for invoice.send_failed events
  4. Try manually resending: POST /api/v1/invoices/{id}/send

Invoice amount looks wrong

  1. Check the rate plan version pinned to the subscription — pricing changes don't apply to existing subscriptions
  2. Verify the billing pipeline stage breakdown in the invoice's Usage Attribution tab
  3. Check for applied credit notes that reduced the total
  4. 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.

SymptomCauseFix
Line shows no status N × 1Events were sent without executionStatus (REST calls or core SDK calls that don't pass it, File Upload)Pass executionStatus on each event, or route traffic through a gateway plugin or an SDK that sets it — see Sending outcomes.
Line shows BLOCKED N × 1 or CANCELLED N × 1Gateways report 401/403/429 as BLOCKED and 499 as CANCELLED. The plan has no weight for them — it was saved before the wizard added them (September 30, 2026), or the weight was left blank — so they bill at 1.0Open the plan in the wizard and enter a weight for Blocked and Cancelled, or add them to outcomeWeights through the rate-plan API.
A status you expected is missing from the breakdownThe status isn't one of the 11 values, so the event was rejected at ingest (or an SDK dropped the status and the event appears under no status)See Events rejected with an invalid executionStatus.
Every call billed at 1.0 and the line has no breakdownAforo couldn't read the per-outcome split when the bill ran; it bills at full weight rather than skip the chargeVoid the invoice and re-run the bill run. If it repeats, contact support — operators see a warning for this case.
Saving the plan fails with Outcome-based pricing needs a SUM or COUNT billable unitThe billable unit aggregates with MAX, AVG or UNIQUE, which have no per-outcome splitUse a SUM or COUNT billable unit.

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:

  1. Open the bill run detail to see per-customer results
  2. Failed items show error codes — common causes:
    • WALLET_INSUFFICIENT_FUNDS — Prepaid customer has insufficient balance
    • USAGE_DATA_UNAVAILABLE — the usage store was temporarily unavailable; re-run the bill run
    • TAX_CALCULATION_FAILED — Avalara timeout; retry the specific invoice

Subscriptions

Subscription stuck in PAST_DUE

Payment has failed and dunning is in progress:

  1. Check dunning attempt count: GET /api/v1/subscriptions/{id}
  2. Manually retry payment: POST /api/v1/subscriptions/{id}/retry-payment
  3. If payment method is outdated, direct customer to update it in the portal
  4. 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 CANCELLED state — it might already be cancelled

Migration fails with 409 Conflict

Common causes:

  • Source subscription is already CANCELLED or EXPIRED — 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

  1. Go to Governance → Integrations → [your integration]
  2. Click Test Connection to get a specific error message
  3. 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:

  1. Click the product → Restore to Gateway
  2. Review the diff (what Aforo would push to the gateway)
  3. 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:

  1. Call await aforo.shutdown() before the process exits. The SDK installs no SIGTERM handler, so a service must call it from its own.
  2. Check network connectivity to the ingest host, and that baseUrl is https:// (plain http is refused except on localhost or with allowInsecureHttp: true).
  3. For a 401/403 logged at error level, fix the API key — the queued usage is sent once a valid key is in place.
  4. Read aforo.droppedCount and the onDrop(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 when shutdownTimeoutMs ran 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-Id header)
  • Your workspace ID (the X-Tenant-Id value)
  • The exact error message and HTTP status code
  • Steps to reproduce