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

The 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 familycompany-id required?
GET /v1/companiesNo (this is how you discover the IDs)
POST /v1/api_keys, GET /v1/api_keysNo (api keys belong to the user, not a company)
GET /v1/users/{id} (your own user)No
GET /v1/permissionsNo (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

SituationStatusBody
Header is omitted on an endpoint that requires it401 Unauthorized{"error": "unauthorized"}
Header is present but the user is not a member of that company404 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 company404 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:

  1. Call GET /v1/companies once on startup or on session boot.
  2. Cache the result in memory (or your usual cache layer) for 1–6 hours.
  3. 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-id to GET /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_id query param).
  • Hardcoding a UUID from another customer's tenant — you will get a 404. Always pull the list from GET /v1/companies.
  • Confusing company-id with the company tax_id (RUT)company-id is always a UUID, never a tax id.

Related