The company-id header 🏢
Almost every endpoint in the Cardda API operates on data that belongs to a company. Cardda is multi-tenant: a single user (and a single API key) can have access to several companies, so every request that touches company-scoped data must say which company it is for. That is what the company-id header is for.
Think of it as the equivalent of the Stripe-Account header for Stripe Connect, or the workspace selector you see in any modern dashboard. It is mandatory for 159 of our 168 public endpoints.
What it looks like
The company-id header is a UUID. It must match the id of one of the companies your user (the owner of the API key) has access to.
curl https://api.cardda.com/v1/banking/bank_transactions \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "company-id: 550e8400-e29b-41d4-a716-446655440000"Both company-id and company_id are accepted, but company-id (with hyphen) is canonical. Use it everywhere.
How to obtain a company-id
company-idThe only endpoint that lists the companies your user has access to is:
GET /v1/companies
This endpoint does not require the company-id header (otherwise you would have a chicken-and-egg problem). Call it once on integration startup and cache the result.
curl https://api.cardda.com/v1/companies \
-H "Authorization: Bearer YOUR_API_KEY"Sample response (truncated):
{
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Acme Holdings",
"legal_name": "Acme Holdings SpA",
"country": "CL",
"tax_id": "76123456-7",
"created_at": "2024-01-15T12:00:00Z"
},
{
"id": "7c1bf5d2-4e2a-4f7e-8c7d-2a3b4c5d6e7f",
"name": "Acme Mexico",
"legal_name": "Acme Mexico SA de CV",
"country": "MX",
"tax_id": "ACM240115AB1",
"created_at": "2024-03-20T09:30:00Z"
}
],
"total": 2
}When the header is required
| Endpoint family | company-id required? |
|---|---|
GET /v1/companies | No (this is how you discover the IDs) |
POST /v1/api_keys, GET /v1/api_keys | No (api keys belong to the user, not a company) |
GET /v1/users/{id} (your own user) | No |
GET /v1/permissions | No (lists permissions across all your companies) |
GET /v1/banking/banks (reference data) | No |
| Everything else (banking, cards, charges, transactions, fiscal invoices, bills, payables, purchase orders, KYC, allocations, statements, etc.) | Yes |
Each endpoint page in the API Reference declares company-id as a required header parameter when it applies.
Errors
| Situation | Status | Body |
|---|---|---|
| Header is omitted on an endpoint that requires it | 401 Unauthorized | {"error": "unauthorized"} |
| Header is present but the user is not a member of that company | 404 Not Found | {"error": "not_found"} (we do not leak whether the company exists) |
| Header is malformed (not a UUID) | 400 Bad Request | {"error": "bad_request", "message": "company-id must be a UUID"} |
| Header references a deleted company | 404 Not Found | {"error": "not_found"} |
Caching pattern
For most integrations, the list of companies a user has access to changes rarely (typically only when a person is invited to a new workspace). A reasonable pattern is:
- Call
GET /v1/companiesonce on startup or on session boot. - Cache the result in memory (or your usual cache layer) for 1–6 hours.
- Refresh on a 401 or 403 from a downstream endpoint.
// Node.js example
let companiesCache = null;
let companiesCachedAt = 0;
const CACHE_TTL = 60 * 60 * 1000; // 1 hour
async function listCompanies(apiKey) {
if (companiesCache && Date.now() - companiesCachedAt < CACHE_TTL) {
return companiesCache;
}
const res = await fetch("https://api.cardda.com/v1/companies", {
headers: { Authorization: `Bearer ${apiKey}` }
});
if (!res.ok) throw new Error(`Cardda /v1/companies failed: ${res.status}`);
const json = await res.json();
companiesCache = json.data;
companiesCachedAt = Date.now();
return companiesCache;
}# Python example
import time, requests
_cache = {"data": None, "ts": 0}
TTL = 60 * 60 # seconds
def list_companies(api_key: str):
if _cache["data"] and time.time() - _cache["ts"] < TTL:
return _cache["data"]
r = requests.get(
"https://api.cardda.com/v1/companies",
headers={"Authorization": f"Bearer {api_key}"},
timeout=10,
)
r.raise_for_status()
_cache["data"] = r.json()["data"]
_cache["ts"] = time.time()
return _cache["data"]Setting it once per request: a small client wrapper
Most SDKs add the header at construction time so you do not forget it on every call:
class CarddaClient {
constructor({ apiKey, companyId }) {
this.apiKey = apiKey;
this.companyId = companyId;
}
async request(path, init = {}) {
return fetch(`https://api.cardda.com${path}`, {
...init,
headers: {
...init.headers,
"Authorization": `Bearer ${this.apiKey}`,
"company-id": this.companyId,
"Content-Type": "application/json",
},
});
}
}
const cardda = new CarddaClient({ apiKey: API_KEY, companyId: COMPANY_ID });
const tx = await cardda.request("/v1/banking/bank_transactions");Switching between companies
If your integration manages multiple companies (e.g. an accounting tool with one Cardda account per client), you do not need to re-authenticate. Just change the company-id header on each request:
# Request against Acme Holdings
curl ... -H "company-id: 550e8400-e29b-41d4-a716-446655440000"
# Same key, different company
curl ... -H "company-id: 7c1bf5d2-4e2a-4f7e-8c7d-2a3b4c5d6e7f"The API key authenticates the user; the company-id header scopes the request.
Common mistakes
- Sending
company-idtoGET /v1/companies— harmless, but pointless. The header is ignored on that endpoint. - Putting it in the request body — the API only reads the header (or, deprecated, a
company_idquery param). - Hardcoding a UUID from another customer's tenant — you will get a
404. Always pull the list fromGET /v1/companies. - Confusing
company-idwith the companytax_id(RUT) —company-idis always a UUID, never a tax id.
Related
- Authentication & API keys — how to generate the key referenced above.
- Getting Started — full quickstart that puts API keys +
company-idtogether. - Errors — the full error catalog.
Updated 1 day ago
