REST APIStatus

Status

After you submit a release, monitor its progress with two endpoints — one for store delivery and one for the background processing job. Both require authentication.

Delivery & aggregate status

curl -H "Authorization: Bearer $ONCE_ACCESS_TOKEN" \
  https://once.app/v1/releases/<id>/status

GET /v1/releases/:id/status returns per-store delivery state plus an aggregate status for the release. Poll this after submission to follow the release out to the stores.

A release that has not reached the distributor yet returns a queued response (aggregateStatus: "queued" with pending: true) instead of an empty result. Errors are specific: a malformed release id, a release that does not exist, and a release owned by another account each return a distinct error message, so your integration can react correctly instead of treating every failure as forbidden. When live store statuses cannot be fetched, the response falls back to the last known statuses and includes fallback: true plus a fallbackReason explaining why.

Processing job

curl -H "Authorization: Bearer $ONCE_ACCESS_TOKEN" \
  https://once.app/v1/releases/<id>/job

GET /v1/releases/:id/job returns the background processing job’s state and any errors. Use it to diagnose a release that is stuck or failed before it reaches the delivery stage.

Release status webhooks

Instead of polling, register webhook endpoints and let ONCE push status changes to you. Webhooks are the recommended pattern; use polling as a fallback or for on-demand checks.

Register up to 5 https endpoints, either in Settings > Developer in the ONCE app or over the REST API. Each endpoint gets a signing secret that is shown once at creation, so copy it immediately.

On every release status change, ONCE sends a POST to each active endpoint with the event release.status_changed:

{
  "event": "release.status_changed",
  "releaseId": "uuid",
  "title": "My Release",
  "artist": "Artist Name",
  "previousStatus": "delivered",
  "status": "distributed",
  "storeStatuses": [
    {
      "storeId": 9,
      "storeName": "Spotify",
      "statusText": "Live",
      "urlInStore": "https://open.spotify.com/album/..."
    }
  ],
  "occurredAt": "2026-07-04T12:00:00.000Z"
}

Every delivery carries two headers:

  • x-once-event: the event type (release.status_changed)
  • x-once-signature: sha256=<hex>, the HMAC-SHA256 of the raw request body computed with your endpoint’s signing secret

Verify each delivery by computing the HMAC-SHA256 of the raw body with your secret and comparing it to the hex digest in x-once-signature.

Delivery and retries: failed deliveries are retried twice. An endpoint that fails 20 deliveries in a row is disabled automatically; you can re-enable it from Settings > Developer, or with a PATCH request, once it is healthy again.

Manage webhooks over the API

Headless integrations can manage webhook endpoints directly with a bearer token (PAT or OAuth), with the same validation and limits as the in-app UI.

List your endpoints (signing secrets are never returned):

curl -H "Authorization: Bearer $ONCE_ACCESS_TOKEN" \
  https://once.app/v1/webhooks

Register an endpoint. The response includes the signing secret exactly once, so store it immediately:

curl -X POST https://once.app/v1/webhooks \
  -H "Authorization: Bearer $ONCE_ACCESS_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"url": "https://example.com/hooks/once", "description": "Production listener"}'

Disable an endpoint, or re-enable one that was auto-disabled after repeated failures (re-enabling resets the failure count):

curl -X PATCH https://once.app/v1/webhooks/<id> \
  -H "Authorization: Bearer $ONCE_ACCESS_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"active": true}'

Delete an endpoint:

curl -X DELETE https://once.app/v1/webhooks/<id> \
  -H "Authorization: Bearer $ONCE_ACCESS_TOKEN"

A POST beyond the 5-endpoint limit returns 409 conflict_error, a non-https URL returns 400 invalid_request_error, and an endpoint id that does not belong to your account returns 404 not_found_error.

Polling guidance

Status moves through asynchronous stages, so poll on an interval rather than expecting an immediate final state. Respect the Retry-After header if you receive a 429 rate_limit_error, and treat a 502 upstream_error as transient — retry with backoff.

Tip: Prefer webhooks over polling when you need to react to status changes. Polling remains fully supported for on-demand checks.