Postal Developers
REST API

Conventions

Base URL, authentication, the response envelope, errors, pagination and limits.

Base URL

https://api.postal.music/api/v1

All public endpoints live under /api/v1. Paths on this site are written relative to that prefix unless shown in full.

Authentication

Send your API key in the X-API-Key header on every request. There are no cookies and no OAuth on the REST surface.

GET /api/v1/tracks/count HTTP/1.1
Host: api.postal.music
X-API-Key: YOUR_API_KEY
StatusmessageCodeMeaning
401missing_api_keyNo header sent
401invalid_keyUnknown, revoked or malformed key
401expired_keyKey past its expiry date
403insufficient_scopeKey lacks the endpoint's scope

Requests from servers and CLIs carry no Origin header and are always accepted. Browser requests are subject to a CORS allowlist that covers Postal's own domains only, so call the API from a backend, not from a web page.

Response envelope

Every successful response is wrapped:

{
  "status": 200,
  "messageCode": 200,
  "message": "Operation was successful",
  "data": { }
}

Your payload is always under data. The outer status is 200 on every success, including creates. Response examples elsewhere on this site show data only.

Some endpoints report a recoverable problem inside a 200 by returning { "ok": false, "error": "TRACK_NOT_FOUND", … } in data rather than an HTTP error. Check ok where the endpoint documents it.

Errors

Errors use a different shape from successes:

{
  "statusCode": 400,
  "messageCode": "not_enough_credits",
  "message": "Not enough credits",
  "error": "Bad Request"
}

Key off messageCode, a stable machine-readable string. message is for humans and can change. Some errors add a data object with structured detail.

In production, validation failures return a generic body:

{ "statusCode": 400, "messageCode": "validation_failed", "message": "Invalid request", "error": "Bad Request" }

Unhandled failures return 500 with messageCode: "internal_error".

Pagination

Three styles are in use. Each endpoint page says which applies.

Cursor (/tracks, /playlists, /composers). Pass limit and, for the next page, cursor set to the previous response's paging.next_cursor. next_cursor is null on the last page. limit is 1–200, default 50.

{ "data": [  ], "paging": { "limit": 50, "next_cursor": "cm1w…" } }

Offset (/tracks/duplicates). Pass limit and offset.

Page (/platform/*). Pass page (from 1) and pageSize. Oversized page sizes are clamped silently.

Rate limits

LimitApplies to
100 requests per 60 seconds per IPEvery endpoint

Exceeding it returns 429. There is no separate per-key limit. Usage per key is logged and visible in the app under Settings → Integrations.

Credits

POST /tracks/search is the only metered endpoint. It runs the AI ranker over your library and costs one credit per call from the key owner's balance. Failed calls are not charged. A caller without credits gets 400 not_enough_credits. Every other endpoint is free.

Request bodies

Send JSON with Content-Type: application/json, except POST /tracks/upload, which is multipart/form-data. Bodies are limited to 10 MB; audio goes through the upload endpoints, not JSON.

Timeouts

Requests time out after 60 seconds with 503 request_timeout. Upload endpoints are exempt.

IDs and dates

IDs are opaque strings. Dates are ISO 8601 in UTC. Large integers are serialised as strings.

Versioning

The /api/v1 prefix is the contract: fields and endpoints are only added within v1, never removed or renamed. Every response carries X-Postal-Api-Version, GET /api/v1/version describes the version without a key, and routes scheduled for removal announce it with standard Deprecation and Sunset headers. See Versioning.

On this page