Parcels — Authentication
Every call to the Parcels API carries an OAuth 2.0 bearer token. This page covers getting one, the scopes that gate this vertical, and revoking a token.
Credentials
You need a client ID and client secret issued for your parcels vendor account. These are machine credentials — there is no browser redirect and no user login.
The client secret authenticates your whole account. Keep it server-side. Never ship it in a mobile app or browser bundle.
Getting a token
Kesk supports exactly one grant: client_credentials. Authenticate with HTTP
Basic — client ID as username, secret as password — and send the grant type as
JSON.
A successful response:
Read scope and expires_in from the response rather than assuming either.
The values above are illustrative — the granted scope can be narrower than
what you asked for, and the token lifetime is set server-side. Never hardcode
a lifetime.
Using the token
Cache the token for its expires_in and refresh on a margin. The token endpoint
does not emit rate-limit headers, but requesting a fresh token per call is
wasteful and slow.
Scopes for Parcels
These are the scopes that gate this vertical. Request the narrowest set that does the job.
| Scope | Grants |
|---|---|
vendors:profile:read | Read the vendor profile |
vendors:settings:read | Read vendor settings |
vendors:locations:read | List and fetch locations |
vendors:locations:write | Create, update and delete locations |
vendors:webhooks:read | List and fetch webhooks |
vendors:webhooks:write | Manage webhooks and rotate secrets |
vendors:bots:read | List bots and read analytics |
vendors:bots:write | Manage bots and rotate secrets |
vendors:orders:read | List and fetch parcel orders |
vendors:packages:read | Read package pricing rules |
vendors:packages:write | Manage package pricing rules |
vendors:units:read | List and fetch autonomous units |
vendors:units:write | Register units, report status, issue machine credentials |
vendors:promotions:read | List the coupons this vendor owns |
vendors:promotions:write | Create, update and delete this vendor's coupons |
vendors:analytics:read | Read sales analytics and performance metrics |
vendors:reviews:read | Read customer reviews |
vendors:promotions:read gates
vendorCoupons;
vendors:promotions:write gates
createVendorCoupon,
updateVendorCoupon and
deleteVendorCoupon.
vendors:analytics:read gates
vendorSalesAnalytics and
vendorPerformanceMetrics;
vendors:reviews:read gates
vendorReviews.
Scopes are matched exactly, or by a vendors:* wildcard on the : boundary.
vendors:* is not a synonym for the coarse grant vendors — the wildcard
grants the whole surface including the sensitive scopes the coarse grant
withholds. Ask for it deliberately, not as shorthand.
The coarse grant vendors expands at issuance into the vendor surface's
granular scopes, minus the eight the registry withholds by name — see
OAuth 2.0 for the list. Those must be requested explicitly,
or granted with a surface wildcard such as vendors:*.
On this vertical that exclusion covers issuing machine credentials, which
vendors:units:write carries: provisionUnitCredential mints a machine
credential and returns its plaintext secret, and a coarse consent must not
silently include the ability to issue new hardware credentials for your fleet.
It also covers vendors:promotions:write: a coupon discounts your own revenue,
and a coarse consent must not silently carry the ability to issue discounts
against your business.
So a coarse vendors grant gives you every scope in the table above except the
two vendors:units:* entries and vendors:promotions:write. Read the granted
scope back from the token response regardless — it is the record of what you
were actually given.
The complete scope registry is published as scopes_supported at
accounts.kesk.app/.well-known/oauth-authorization-server.
Note the host: that is the authorization server, not the API host, so the
path does not resolve on api.kesk.app. The table above is the subset
that gates this vertical; the registry also lists the scopes of every other
surface. The scope in your token response remains the record of what you were
actually granted.
Errors
| Situation | Status | Body |
|---|---|---|
Missing Authorization header | 401 | {"title":"Unauthorized","message":"Authentication required.","status":401,"type":"unauthorized"} |
Wrong client ID or secret | 401 | {"error":"invalid_client","error_description":"Client authentication failed."} |
Unsupported grant_type | 400 | {"error":"unsupported_grant_type","error_description":"Only 'client_credentials' grant type is supported."} |
Scope not on your registration | 400 | {"error":"invalid_scope","error_description":"Scope exceeds the client's registered grant: ..."} |
Form-encoded body on /token | 400 | {"error":"Bad Request","message":"invalid content type ..., expect: application/json"} |
Validation order is grant_type → client → scope, so a bad grant type is
rejected before your credentials are checked.
Note the missing-header and wrong-content-type cases return plain text — parsing either as JSON will throw.
Revoking a token
The token endpoint takes JSON; the revoke endpoint takes
application/x-www-form-urlencoded. Sending JSON here returns 415.
token_type_hint is optional and ignored, and access_token is the only value
the endpoint will parse — any other string is a 400. Revocation applies to
access tokens: passing a refresh token returns 200 and revokes nothing.
Revocation kills the entire token family, not just the string you passed — which is what you want when responding to a leak.
Per RFC 7009 the endpoint returns 200 even for an unknown token, so a 200
means "not valid any more" rather than "existed and was revoked". Treat it as
idempotent and safe to retry.
To rotate a secret: issue and deploy the new credentials, drain in-flight work, then revoke the old token. Revoking first breaks every worker still holding it.
Reference
Generated reference — every type, field and argument, built from the live schema.