Skip to main content

Internal API v1

The Internal API gives Publica.la teams (Admin, Support, Product) programmatic access to Farfalla data and actions, separate from the customer-facing v1, v2, and v3 APIs. It runs as a Bearer-token API mounted at internal-api/v1/* on the platform host only.

At a glance:

  • Bearer token authentication, scoped to API clients (not to users)
  • Per-token abilities (tenants:read, tenants:write, cross-tenant) gate every route
  • 60 requests per minute per token
  • Tokens do not expire. Rotation happens through revoke and re-issue

Base URL

https://app.publica.la/internal-api/v1/

The API only answers on the platform host (app.publica.la). Requests that resolve to any other host return 404. There is no per-tenant subdomain entry point.


Authentication

Every request must carry a Bearer token in the Authorization header.

Authorization: Bearer <id>|pla_int_<secret>

Tokens carry a pla_int_ prefix to make them recognizable to secret-scanning tools. The full token is shown once at issuance and never again. Only a hash and the last four characters are stored on the server.

The Internal API does not read the X-User-Token header. A request that authenticates with X-User-Token instead of a Bearer returns 401.

Issuance

Token issuance is restricted to engineering. The flow is:

  1. Engineering opens the Api Clients panel in Nova.
  2. Picks an existing client or creates a new one and runs the Issue token action.
  3. Selects the abilities to grant.
  4. The plaintext token appears once on a one-shot reveal page.

There is no self-service flow and no public issuance endpoint. Coordinate with engineering to receive a token.

Revocation

Engineering revokes a token through Nova. The token stops working on the next request, with no graceful-period or warning state.


Abilities

Each token carries a set of abilities. Routes declare which abilities they require. A token without the required ability gets a 403:

{ "message": "Invalid ability provided." }
AbilityRequired by
tenants:readReading the tenant directory
tenants:writeMutating tenant state (mark/clear missing payments)
cross-tenantEndpoints whose URL has no {tenant} segment

Naming convention: <resource>:<action>. New abilities ship alongside the endpoints that introduce them.


Tenant scoping

URLs come in two shapes.

Tenant-scoped: the URL names the customer tenant

/internal-api/v1/tenants/{tenant}/missing-payments

The {tenant} segment names the customer tenant the call acts on. The token's tenants:write ability authorizes the action.

Operations against the platform tenant (id 1) are rejected with 403 and the message Operations on the root tenant are not allowed via internal-api. Internal API endpoints target customer tenants, not the platform tenant itself.

Cross-tenant: the URL has no tenant segment

/internal-api/v1/tenants

These routes return data spanning every tenant on the platform. They require the cross-tenant ability in addition to the resource-level read or write ability.

The X-Farfalla-Tenant-Id header is not used by the Internal API.


Headers reference

HeaderDescriptionRequired
AuthorizationBearer <id>|pla_int_<secret>All endpoints
Acceptapplication/jsonRecommended
Content-Typeapplication/jsonPOST, PUT, PATCH

The API always responds with JSON, including for error responses (auth failures, throttling, host rejections).


Pagination

List endpoints use cursor pagination.

{
"data": [...],
"links": {
"next": "https://app.publica.la/internal-api/v1/tenants?cursor=eyJpZCI6MTQs...",
"prev": null
},
"meta": {
"has_more": true
}
}

Pass the links.next URL back as-is for the following page. Iterate until meta.has_more is false. Treat the cursor value as opaque, its format is an implementation detail and may change without notice.


Response conventions

  • Ids are strings. Every resource id in a response is serialized as a string. Keeps JavaScript clients precision-safe, since Number loses precision above 2^53.

Rate limiting

Each token has a 60 RPM budget per calendar minute. A token over budget gets 429 until the window resets.

HTTP/1.1 429 Too Many Requests
Retry-After: <seconds until window resets>

{
"message": "Too Many Attempts."
}

Audit log

Every authenticated call is recorded for review. Engineering can surface call history per API client when investigating issues.


Error handling

CodeMeaning
200Success
400Domain refusal (the action cannot proceed against current state)
401Missing or invalid Bearer
403Token lacks the required ability, or the URL targets the platform tenant
404Wrong host, or {tenant} not found
422Request body validation failure
429Rate-limit budget exhausted
500Unhandled server error

Errors come in two envelope shapes:

  • Domain refusals (400) carry a stable machine-readable code: { "error": { "code": "...", "message": "..." } }. Match on error.code.
  • Framework rejections (auth, ability, host, throttle, route binding) carry a single message string: { "message": "..." }. Match on the status code, not on the message text.

Domain refusal (400)

Endpoint-specific validation that the request cannot proceed. The envelope includes a stable machine-readable code:

{
"error": {
"code": "tenant_has_no_stripe_customer",
"message": "Tenant has no stripe customer."
}
}

Auth failure (401)

{ "message": "Unauthenticated." }

Ability failure (403)

{ "message": "Invalid ability provided." }

Root-tenant guard (403)

{ "message": "Operations on the root tenant are not allowed via internal-api." }

Not found (404)

{ "message": "Not found" }

Rate limit (429)

HTTP/1.1 429 Too Many Requests
Retry-After: <seconds until window resets>

{
"message": "Too Many Attempts."
}

Endpoint summary

Tenants

MethodEndpointRequired abilitiesDescription
GET/tenantstenants:read, cross-tenantList tenants with an active plan
POST/tenants/{tenant}/missing-paymentstenants:writeFlag a tenant as having unpaid invoices
DELETE/tenants/{tenant}/missing-paymentstenants:writeClear the unpaid-invoices flag

Next steps


See also

X

Graph View