Use one token for every entity of a Spendesk organisation: list its companies, then scope each call to the entity you want.
Many Spendesk customers run several legal entities — one company (wallet) per country or subsidiary — grouped under one organisation. A company-level credential only reaches its own company, so an integration covering the whole group would need one connection per entity.
An organisation-level credential removes that: with a single token you list the organisation's companies, then choose, on each call, which company you act on.
After following this guide you will be able to:
- create an organisation-level API key, or connect an OAuth2 app at organisation level;
- list the companies of the organisation with Get Companies;
- call any
v1endpoint for one company using theX-Company-Idheader; - call the company-scoped
v2endpoints, where the company is part of the path.
ExperimentalOrganisation-level access relies on
experimental:*scopes. Request access to experimental features from your CSM or by email at [email protected].
Company-level vs organisation-level tokens
A token carries its level from the credential it was issued for: an API key or OAuth2 connection created for a company gives company-level tokens; one created for the organisation gives organisation-level tokens.
| Behaviour | Company-level token | Organisation-level token |
|---|---|---|
| Companies reachable | Its own company | Every company of the organisation (see step 2) |
v1 endpoints | Called as documented | Require the X-Company-Id header |
| Get Companies | 403 — This endpoint requires an organisation-level token | Available — the only endpoint that needs no company |
/v2/companies/{companyId}/… | Only its own company; another id returns 403 | Any company of the organisation |
Prerequisites
- An organisation-level credential — see step 1.
- The scope
experimental:company:read, to list companies. - The scopes of the endpoints you will call for each company — for example
payable:readfor payables orexperimental:invoice:readfor invoices. See Scopes. A token only carries scopes granted to its credential.
1. Get an organisation-level credential and token
With an API key (customers)
Organisation-level API keys are created in Spendesk by an organisation owner, in Settings → Integrations → API Access Management — every organisation owner has access to it, and other users are refused (403). As with company keys, you choose the scopes and an expiry date.
Then request tokens exactly as with a company key, through /auth/token:
curl --request POST \
--url https://public-api.spendesk.com/v1/auth/token \
--header 'accept: application/json' \
--header 'authorization: Basic bXlfaWQ6bXlfc2VjcmV0' \
--header 'content-type: application/json' \
--data '{ "grant_type": "client_credentials" }'{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.masked",
"token_type": "Bearer",
"expires_in": 3600
}With OAuth2 (partners)
Your app runs the usual PKCE flow described in How to Authenticate — authorize, then token. On the Spendesk consent screen, the customer can approve the connection for the whole organisation instead of a single company; the tokens you then receive, and their refreshed successors, are organisation-level. The organisation-wide option can be approved by an Account Owner, an Admin or a Controller — a Requester is refused. No specific setting is needed on your app.
With an OAuth2 organisation token, every call also checks that the user who approved the connection is still an active member of the company you target, with a role allowed on that endpoint. If they were removed from that company, the call returns 403 — User does not belong to this company.
2. List the companies of the organisation
Call Get Companies:
curl --request GET \
--url 'https://public-api.spendesk.com/v2/companies?page=1&pageSize=30' \
--header 'accept: application/json' \
--header 'authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.masked'{
"data": [
{ "id": "7j6mvn11rqf7j5", "name": "Acme France" },
{ "id": "3k2pq88wlm0a1c", "name": "Acme GmbH" }
],
"meta": {
"pagination": { "page": 1, "pageSize": 30, "total": 2 }
}
}What the list contains depends on the credential:
- API key — every active company of the organisation;
- OAuth2 — only the active companies the user who approved the connection is a member of.
The list is cached for 20 minutes (1,200 seconds): a company created or deactivated in Spendesk can take that long to appear or disappear. Page through it as described in Pagination. Store the company ids: they are the values you pass on every following call.
3. Call v1 endpoints for one company
v1 endpoints for one companyWith an organisation-level token, every v1 endpoint requires the X-Company-Id header, naming the company the call acts on:
curl --request GET \
--url https://public-api.spendesk.com/v1/wallet-summary \
--header 'accept: application/json' \
--header 'authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.masked' \
--header 'X-Company-Id: 7j6mvn11rqf7j5'| Situation | Response |
|---|---|
| Header missing | 400 — detail: X-Company-Id header is required for organisation tokens, source: headers.x-company-id |
| Company not part of the organisation | 403 — Company does not belong to organisation |
| OAuth2 token, approving user no longer a member | 403 — User does not belong to this company |
To cover the whole organisation, repeat the call once per company with the same token — there is no need to request a token per company. With a company-level token the header is not needed and is ignored.
4. Call company-scoped v2 endpoints
v2 endpointsThe v2 invoice and intake endpoints take the company in the path: /v2/companies/{companyId}/…. The path is authoritative — no X-Company-Id header is needed, and one sent anyway is ignored. For example, List invoices (scope experimental:invoice:read):
curl --request GET \
--url 'https://public-api.spendesk.com/v2/companies/7j6mvn11rqf7j5/invoices?limit=50&sortBy=updatedAt&sort=desc' \
--header 'accept: application/json' \
--header 'authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.masked'These endpoints use cursor pagination: pass limit (1 to 100, default 20), then follow meta.pagination.nextCursor — or directly meta.pagination.nextUrl, which keeps your filters — until hasNextPage is false:
{
"data": [ { "id": "…", "invoiceNumber": "F-2026-0042", "…": "…" } ],
"meta": {
"pagination": {
"limit": 50,
"hasNextPage": true,
"nextCursor": "eyJvZmZzZXQiOjUwfQ",
"nextUrl": "https://public-api.spendesk.com/v2/companies/7j6mvn11rqf7j5/invoices?limit=50&sortBy=updatedAt&sort=desc&cursor=eyJvZmZzZXQiOjUwfQ"
}
}
}The same pattern applies to intakes — List intakes, Create an intake and its file upload steps.
The other v2 endpoints — List Analytical Fields and the /v2/expense-category-fields family — have no company in the path: with an organisation-level token they need the X-Company-Id header, like v1.
Rate limits
Per-minute limits are counted per company and per credential: with an organisation-level token, each company you name in X-Company-Id (or in the path) has its own budget of 1,000 requests per minute, so calls for one entity do not consume another's. Get Companies, which names no company, is counted at organisation level. See Rate Limiting for the figures, and spread your per-company calls over time rather than looping over every company at the same instant.
Status of these endpoints
Organisation-level access and the v2 endpoints are experimental: they require experimental:* scopes and may change.
Next steps
- Get Companies, List invoices, List intakes — full request and response reference.
- Pagination and Error Handling.
- Scopes — the scopes to request for each endpoint.