Authentication

Bearer tokens for the REST API.

Every call to api/v1 carries one header — Authorization: Bearer pkd_…. No signing, no nonces, no clock skew to worry about. HTTPS handles the transport. The public passport feed under api/public/v1 carries nothing at all.

Token shape

A token is pkd_ followed by 48 hex characters (24 random bytes). It is opaque — don't parse it. onpack stores only a SHA-256 digest, so the plaintext is shown exactly once, when it is issued.

Tokens are named and a company can hold as many as it likes — one per integration, so revoking the one a contractor used doesn't take the ERP feed down with it. Each one carries:

AttributeMeaning
ScopesOne or more of dpp:read, dpp:write, marketing:read, marketing:write. dpp is Pack Identity — the passport, its revisions, the passport.published and document.expiring events. marketing is Pack Experience — units, batches, scans, collectors, redemptions, webhook endpoints. The catalogue (/brands, /skus, /edits) accepts either product's grant at the needed access level.
BrandOptional. Left blank, the token reaches every brand the company owns; set, it reaches that brand only — anything else answers 404, so a narrowed token can't be used to enumerate the rest of the portfolio.
Issued byThe team member who created it. Every passport revision written through the API records both the token and that person.

Issuing one

Sign in, open the brand dashboard, go to Settings → API, give the token a name, tick its scopes, optionally pin it to one brand, and click Issue token. Copy the value from the banner — it is not shown again. Only an admin or an account manager on that brand sees the page.

Sending the token

curl
curl https://onpack.io/api/v1/brands \
  -H "Authorization: Bearer pkd_••••••••••••••••"
Ruby
require "net/http"
uri = URI("https://onpack.io/api/v1/brands")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{ENV['ONPACK_API_TOKEN']}"
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
Node
const res = await fetch("https://onpack.io/api/v1/brands", {
  headers: { Authorization: `Bearer ${process.env.ONPACK_API_TOKEN}` }
});

Responses you should expect

StatusMeaning
200 / 201Request succeeded. Body is JSON; 204 on a delete.
400A parameter could not be read — a since that isn't ISO 8601, for example. error says which.
401Missing, unknown, revoked or expired token. Deliberately one message for all four.
402Not a permission problem — a plan one. error is product_not_in_plan when the company doesn't own the product this endpoint belongs to, overage_confirmation_required when a batch would exceed the included units (re-send with confirm_overage=true), or the reason code generation is paused on the account.
403insufficient_scope. The token exists but wasn't granted this permission; needed and granted in the body say which.
404Resource not found or outside this token's brand.
422Validation failed. Field-level messages in details.
429rate_limited. Over the ceiling below; Retry-After (seconds) and retry_after in the body say when the window opens again.
JSON
{
  "error":   "insufficient_scope",
  "needed":  "dpp:write",
  "granted": ["dpp:read", "marketing:read"],
  "message": "This token does not have the dpp:write permission."
}

Rate limits

60 requests a minute per token, across every endpoint, reads and writes alike. Beyond that every request answers 429 until the minute turns over. A catalogue sync that writes one product a second sits exactly on the line — space calls out, or batch what you can: a passport PATCH carries every field in one call, and the catalogue reads page at up to 1,000 products a call. The unauthenticated passport feed has its own ceiling of 60 a minute per IP.

HTTP
HTTP/1.1 429 Too Many Requests
Retry-After: 23
Content-Type: application/json

{ "error": "rate_limited", "retry_after": 23,
  "message": "Rate limit exceeded: 60 requests a minute. Try again in 23 seconds." }

Rotating a token

Issue the new token first, deploy it, then Revoke the old one from Settings → API. Revocation is immediate; because tokens are individual there is no forced overlap window — you choose it. A revoked token stays listed with who issued it and when it was last used.

!
Store tokens in a secrets manager, never in source control. A token with marketing:write can mint production codes that go to a printer; treat it like a payment credential.