Portal API

The REST surface under the libraries: portal-token-authenticated, browser-callable, rate-limited by design.

The Portal API is what @repost/portal-js and the hooks speak. You rarely call it by hand, since the libraries add token refresh, backoff, and typed responses, but the contract is public and stable.

  • Base URL: https://api.repost.sh/portal/v1/
  • Auth: Authorization: Bearer <portal access token>, the short-lived, per-customer tokens your backend mints. The token implies the customer and environment; they never appear in URLs.
  • CORS: open. The token is the credential, so the API is callable directly from any customer dashboard origin.
  • Format: JSON, ISO-8601 timestamps.

Errors

Every non-2xx response carries one envelope:

{ "error": { "code": "not_found", "message": "Delivery not found" } }
CodeStatusMeaning
unauthorized401Missing, invalid, expired, or revoked token. Mint a fresh one.
forbidden403Read-only session hit a mutation-tier route, or the feature is disabled.
not_found404The resource doesn't exist, or isn't owned by the token's customer.
validation_error400Bad input; error.issues lists { path, message } per field.
conflict409State race (e.g. a replay job already active on the endpoint).
rate_limited429Budget exhausted. Honor Retry-After.
internal500Our fault; the message carries a trace id for support.

Rate limits

Budgets are cost-weighted (a search costs more than a cheap read, a replay more than a search) and enforced per customer, per environment, and globally. Every response reports the most constrained budget:

HeaderMeaning
RateLimit-LimitSustained budget, tokens per minute.
RateLimit-RemainingTokens left right now.
RateLimit-ResetSeconds until fully refilled.
Retry-AfterOn 429: seconds to wait.

Pace on RateLimit-Remaining before you ever see a 429. For live data, use the realtime feed instead of polling. The libraries already do both.

Routes

AreaRoutes
ContextGET /context: customer identity, branding, read-only flag.
LogsGET /logs, GET /logs/histogram, GET /logs/check-newer: the delivery-attempt feed and its chart.
DeliveriesGET /deliveries/{id}/attempts · POST /deliveries/{id}/replay (202).
DLQGET /dlq, GET /dlq/count, GET /dlq/count-by-endpoint.
EndpointsGET /endpoints · POST /endpoints (201, returns the signing secret once) · PUT/DELETE /endpoints/{id} · POST .../pause, .../resume · POST .../secret/reveal, .../secret/rotate · GET .../active-replay-job.
Replay jobsPOST /replay-jobs/estimate · POST /replay-jobs (prepare, 201) · GET /replay-jobs/{id}, GET .../items · POST .../confirm, .../cancel, .../pause, .../resume.
Event typesGET /event-types · POST /event-types/{id}/send-sample: a signed sample to an endpoint or a one-off URL.
RealtimePOST /realtime/token: mint a websocket tail token.

List routes paginate with an opaque cursor and return nextCursor / hasMore.

Read-only tokens can call every GET plus the realtime mint; everything else answers 403 forbidden, secret reveal included.

Realtime

POST /realtime/token returns { token, realtimeUrl, expiresAtMs } for the websocket feed of the customer's delivery attempts. The protocol is an implementation detail of the libraries. Subscribe through portal.realtime.subscribeLogs or useLogsFeed rather than hand-rolling it.

The full contract

The OpenAPI document ships with @repost/portal-js (its types are generated from it) and in the open-source repo alongside the registry sources.