Authentication

The Fenicia public API (api.fenicia.io) receives the credential in the Authorization: Bearer <credential> header. That same endpoint accepts two kinds of credential: the session JWT used by the dashboard (app.fenicia.io), and the API key (fkapi_...), which is the credential supported for external integrations.

This section documents the API key flow — it's the only mechanism you should use if you're integrating programmatically against Fenicia.

Hosts and base URLs

HostRole
api.fenicia.ioThe public API. All your business requests go here (orders, products, inventory, etc.). Base URL: https://api.fenicia.io, with no version prefix (there is no /v1).
account.fenicia.ioIssues the dashboard's session JWT and hosts API key management (create, list, revoke, delete). Not used to consume the public API. Note it's account, singularaccounts.fenicia.io does not resolve.
webhooks.fenicia.ioReceives inbound webhooks from third parties (marketplaces, payment gateways). It plays no part in authenticating your outbound integration.

Key management does not live on api.fenicia.io

Creating, listing, revoking, or deleting an API key are dashboard operations, not public API routes. The only supported way to manage them today is the Fenicia dashboard — see API Key Management.

Environments

The rate-limit table further below distinguishes dev/sandbox/prod, but each environment lives on a different host — your API key for a test tenant doesn't work against the production host, and vice versa:

EnvironmentHostStatus (verified live)
Productionhttps://api.fenicia.ioReturns 401 with no credential — live.
Developmenthttps://api-dev.fenicia.ioReturns 401 with no credential — live.
SandboxDNS for api-sandbox.fenicia.io does not resolve today (NXDOMAIN). Even though a sandbox row exists in the rate-limit table, there is no public host to point at for this environment yet.

Sandbox has no public host today

If you need a testing environment before integrating against production, use api-dev.fenicia.io with an API key from a development tenant. The sandbox row in the rate-limit table documents a limit configured at the infrastructure level, not an available host to connect to — don't use it as a reference for where to point your integration.

How the credential is sent

Every authenticated request carries the API key in the Authorization header using the Bearer scheme:

Authorization: Bearer fkapi_1m8x2z9k_3f9a1c2d4e5f6789abcd0123ef456789
curl https://api.fenicia.io/orders \
  -H "Authorization: Bearer $FENICIA_API_KEY" \
  -H "Content-Type: application/json"

What an authentication failure returns

The api.fenicia.io authorizer does not return a JSON body with its own error-code catalog: when it denies a request, it generates an IAM deny policy, and API Gateway translates that to AWS's default body — {"message": "<detail>"} — with no code field. No GatewayResponse has a custom template configured for these cases.

SituationHTTPBody
Authorization header missing or doesn't match the Bearer <token> format401{"message": "..."}
API key doesn't exist, was revoked, or lacks sufficient permissions403{"message": "..."}
Tenant is in blocked or terminated state403{"message": "..."} — blocked by the authorizer before reaching the handler
Tenant is suspended (or terminated, on the few routes the authorizer still lets through) and the operation is a write402{"code": "TENANT_PAYMENT_REQUIRED", "message": "..."}

The only structured code is TENANT_PAYMENT_REQUIRED

If you're automating error handling against api.fenicia.io, don't rely on a catalog of authentication error codes — it doesn't exist. The only case with a structured code in the response body is the billing block (TENANT_PAYMENT_REQUIRED, HTTP 402), emitted by the business handler, not the authorizer. Everything else arrives as a generic 401/403. A tenant in payment-required (the dunning grace period) is not blocked — its writes still go through.

There is no single response envelope across the whole API

Check the domain's envelope before you write your parser

Fenicia doesn't have a homogeneous response contract across domains. Each one documents its own in its own article — read this table before assuming what you saw in one domain applies to another.

DomainSuccess envelopeError envelopeDetail
Orders (/orders/*){ "data": ..., "meta": {...} } (on a single resource, meta is optional; meta.pagination is mandatory on lists){ "error": { "code": "namespace:reason", "message": "..." } }Orders overview
Products (/products/*)No single envelope — flat array, direct object, or ad-hoc shape depending on the endpoint; only one endpoint uses {data, meta}Varies by endpointProducts overview
Collections (/collections/*)Flat body (follows the Products legacy pattern)Varies by endpointCollections
Categories (/categories/*){ "success": true, ... }{ "success": false, "error": "<text>", "code": "SCREAMING_SNAKE" } — note: here error is a string, not an object, and code IS uppercase, the opposite of OrdersCategories
Inventory (/inventory/*)Resource-specific shape ({ items, pagination }, { transfers, pagination }, etc.), raw document on detail endpointsVaries by endpointInventory overview
Authentication (authorizer failures)N/AAWS's { "message": "..." }, no code (except TENANT_PAYMENT_REQUIRED)This page, above

Errors that don't come from Fenicia

Some edge responses are generated by infrastructure, not the platform

Some requests never reach a Fenicia handler at all — an infrastructure layer (API Gateway, or the load balancer in front of it) rejects them before that. Those responses don't follow any of the platform's envelopes: they carry no data/error object, only AWS's {"message": "..."} — and the 414 case isn't even JSON. If your client assumes every response from api.fenicia.io is JSON shaped like the table above, these cases will break it.

SituationHTTPReal body (verified)
Route outside the known route tree (e.g. /kittens), without an Authorization header403{"message":"Missing Authentication Token"} — AWS's JSON, with no code
Route outside the known route tree, with an Authorization header403API Gateway tries to read the header as AWS SigV4 and fails: {"message":"Invalid key=value pair (missing equal-sign) in Authorization header (hashed with SHA-256 and encoded with Base64): '<hash>'."} — still JSON, but it's about SigV4, not about your API key
Invalid or revoked API key on a route that does exist403{"message":"User is not authorized to access this resource with an explicit deny in an identity-based policy"}
Request URI too long414HTML, not JSON — generated by the load balancer (Server: awselb/2.0): <html><head><title>414 Request-URI Too Large</title></head><body><center><h1>414 Request-URI Too Large</h1></center></body></html>

A 403 on an unknown route is not a credentials problem

If you mistype a path, API Gateway replies with a SigV4 parsing complaint that mentions the Authorization header. It reads as if your API key were wrong — it isn't. Check the path first: a route that doesn't exist in the tree never reaches Fenicia's authorizer.

Don't parse these cases as if they were Fenicia's

If your client calls response.json() without checking Content-Type first, the long-URI case (HTML) will blow up the parse. Check the HTTP status and Content-Type before assuming the body is JSON in the platform's envelope.

Rate limiting

The rate limit is a throttle at the API Gateway stage level — a token bucket shared across every client in that environment —, not a per-IP or per-API-key limit. The real values vary by environment:

EnvironmentRate (requests/second)Burst
dev100200
sandbox5001000
prod20005000

Exceeding the limit returns 429 Too Many Requests. As with authentication errors, there's no custom response template configured for this case.

Tip

Implement retries with exponential backoff on a 429. Since the limit is shared across the entire environment (not per API key), a traffic spike from another client in your same environment can push you toward the limit even if your integration stays steady.

Next steps