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" } }| Code | Status | Meaning |
|---|---|---|
unauthorized | 401 | Missing, invalid, expired, or revoked token. Mint a fresh one. |
forbidden | 403 | Read-only session hit a mutation-tier route, or the feature is disabled. |
not_found | 404 | The resource doesn't exist, or isn't owned by the token's customer. |
validation_error | 400 | Bad input; error.issues lists { path, message } per field. |
conflict | 409 | State race (e.g. a replay job already active on the endpoint). |
rate_limited | 429 | Budget exhausted. Honor Retry-After. |
internal | 500 | Our 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:
| Header | Meaning |
|---|---|
RateLimit-Limit | Sustained budget, tokens per minute. |
RateLimit-Remaining | Tokens left right now. |
RateLimit-Reset | Seconds until fully refilled. |
Retry-After | On 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#
| Area | Routes |
|---|---|
| Context | GET /context: customer identity, branding, read-only flag. |
| Logs | GET /logs, GET /logs/histogram, GET /logs/check-newer: the delivery-attempt feed and its chart. |
| Deliveries | GET /deliveries/{id}/attempts · POST /deliveries/{id}/replay (202). |
| DLQ | GET /dlq, GET /dlq/count, GET /dlq/count-by-endpoint. |
| Endpoints | GET /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 jobs | POST /replay-jobs/estimate · POST /replay-jobs (prepare, 201) · GET /replay-jobs/{id}, GET .../items · POST .../confirm, .../cancel, .../pause, .../resume. |
| Event types | GET /event-types · POST /event-types/{id}/send-sample: a signed sample to an endpoint or a one-off URL. |
| Realtime | POST /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.