{"openapi":"3.2.0","info":{"title":"TimeFusion Integrations API","version":"1.2.0","description":"## Quickstart\n\nThe TimeFusion Integrations API exposes an organization's time bookings and the\nmaster data around them, lets an external invoicing system mark bookings as\nbilled, and lets an ERP or CRM keep customers, projects and hourly rates in step\nwith its own. Creating and editing bookings stays in the app.\n\nEvery endpoint lives under `/api/integrations/v1` on your TimeFusion origin, the\n`servers` entry on this page. Prose below writes paths without that prefix\n(`GET /time-bookings`); the examples spell out the whole URL.\n\n### Pick your path\n\nEvery integration starts the same way: an administrator switches the API on and\ncreates a key with the scopes your integration needs\n([Authentication](#description/authentication)), and your first request is `GET /me`\n([Your first call](#description/your-first-call)). From there:\n\n| You are building | Scopes | Read |\n| --- | --- | --- |\n| **Invoicing**: bill finished work from your invoicing system | `bookings:read`, `billing:write`, `billing:read`; `money:read` for amounts | [Reading data](#description/reading-data), then [Billing workflow](#description/billing-workflow) |\n| **Mirror or report**: copy bookings into BI, payroll or controlling | `bookings:read`, `masterdata:read`, `users:read` as needed; `money:read` for amounts | [Reading data](#description/reading-data), then [Keeping in sync](#description/keeping-in-sync) |\n| **Master-data push**: keep customers, projects and rates in step with an ERP or CRM | `customers:write`, `projects:write`, `rates:write` as needed, `masterdata:read`; `money:read` for rates | [Syncing master data](#description/syncing-master-data), then [Keeping in sync](#description/keeping-in-sync) to read changes back |\n\n[API conventions](#description/api-conventions) (parameters, pagination, rate limits, errors,\nversioning) applies to all three.\n\n**Rehearse while you are building.** The billing write and every batch take\n`\"dryRun\": true`, which answers exactly as the real call would and writes\nnothing. A real billing write locks the booking for the employee who recorded it\n([Billing freezes the booking for the employee](#description/billing-freezes-the-booking-for-the-employee)).\n\n### Rules every integration follows\n\nEach line links to the section that explains it.\n\n- **Read `GET /me` at startup:** `organization.currency` (never hard-code\n  `EUR`), `organization.timeZone`, `money.visible` and `key.expiresAt`.\n  [Your first call](#description/your-first-call).\n- **Ignore unknown fields, and keep a default case** for every vocabulary such\n  as `error.code`, `kind` or `Rate.source`: `v1` grows by adding.\n  [Versioning](#description/versioning).\n- **Branch on `error.code` and `details[]`, never on `message`.**\n  [Errors](#description/errors).\n- **Repeat a parameter to send several values** (`?projectId=a&projectId=b`);\n  bracket forms are a `400`. [Query parameters](#description/query-parameters).\n- **Follow `nextCursor` until `hasMore` is false**, and never build, parse or\n  move a cursor. [Pagination](#description/pagination).\n- **Key your records on ids, not names.** A rename does not move the\n  `updatedAt` of the records that reference it.\n  [Finding late edits: `sort=updatedAt`](#description/finding-late-edits).\n- **Resume a `sort=updatedAt` walk with an overlap**, and save the checkpoint\n  only once the whole walk succeeded. [Finding late edits: `sort=updatedAt`](#description/finding-late-edits).\n- **Wait for `Retry-After` on a `429`.** [Rate limits](#description/rate-limits).\n- **Never unmark, re-point or rebill automatically.** A lost response is\n  retried with the same `Idempotency-Key`; anything else needs a person.\n  [Retrying safely: `Idempotency-Key`](#description/retrying-safely).\n- **Keep the key on your backend.** [Keys stay on your backend](#description/keys-stay-on-your-backend).\n\n\n### Endpoints at a glance\n\n| Endpoint | Scope | Purpose |\n| --- | --- | --- |\n| `GET /me` | none | Introspect the calling API key |\n| `GET /time-bookings` | `bookings:read` | List time bookings in a period |\n| `GET /time-bookings/{id}` | `bookings:read` | Fetch one time booking by id |\n| `GET /projects` | `masterdata:read` | List projects |\n| `GET /projects/{id}` | `masterdata:read` | Fetch one project by id |\n| `GET /projects/external/{externalId}` | `masterdata:read` | Fetch one project by externalId |\n| `GET /projects/{id}/hours` | `bookings:read` | Fetch a project's hours |\n| `GET /tasks` | `masterdata:read` | List tasks |\n| `GET /tasks/{id}` | `masterdata:read` | Fetch one task by id |\n| `GET /customers` | `masterdata:read` | List customers |\n| `GET /customers/{id}` | `masterdata:read` | Fetch one customer by id |\n| `GET /customers/external/{externalId}` | `masterdata:read` | Fetch one customer by externalId |\n| `GET /rates` | `masterdata:read` | List hourly rates |\n| `GET /rates/{id}` | `masterdata:read` | Fetch one hourly rate by id |\n| `GET /rates/external/{externalId}` | `masterdata:read` | Fetch one hourly rate by externalId |\n| `GET /users` | `users:read` | List people |\n| `GET /users/{id}` | `users:read` | Fetch one person by id |\n| `POST /rates` | `rates:write` | Create an hourly rate |\n| `PATCH /rates/{id}` | `rates:write` | Update an hourly rate by id |\n| `PUT /rates/external/{externalId}` | `rates:write` | Upsert an hourly rate by externalId |\n| `POST /rates/batch` | `rates:write` | Upsert hourly rates by externalId, in batch |\n| `POST /customers` | `customers:write` | Create a customer |\n| `PATCH /customers/{id}` | `customers:write` | Update a customer by id |\n| `PUT /customers/external/{externalId}` | `customers:write` | Upsert a customer by externalId |\n| `POST /customers/batch` | `customers:write` | Upsert customers by externalId, in batch |\n| `POST /projects` | `projects:write` | Create a project |\n| `PATCH /projects/{id}` | `projects:write` | Update a project by id |\n| `PUT /projects/external/{externalId}` | `projects:write` | Upsert a project by externalId |\n| `POST /projects/batch` | `projects:write` | Upsert projects by externalId, in batch |\n| `POST /time-bookings/billing` | `billing:write` | Mark or unmark time bookings as billed |\n| `GET /invoices` | `billing:read` | List invoices |\n| `GET /invoices/{id}` | `billing:read` | Fetch one invoice by id |\n\n`none` means no *scope*, not no key: `GET /me` accepts any valid key, even one\nwith no scopes. Scopes never imply one another: billing and then reconciling\nneeds both `billing:write` and `billing:read`. Each operation also publishes\nits scopes as `x-required-scopes` in `openapi.json`.\n\nThe documentation routes (this page, `openapi.json` and the starter kit) are\npublic and take no key.\n\n### Starter kit\n\n[Download the starter kit (.tar.gz)](/api/integrations/v1/starter-kit.tar.gz):\na runnable Node.js integration for your backend, with every example below, an\nenvironment template and a README. No dependencies: Node 24 runs its TypeScript\nas it is.\n\n**To build your own integration, copy\n[client.ts](/api/integrations/v1/starter-kit/client.ts) into your\nbackend.** It is one file with a typed method for every operation, plus the\npagination, retries, conditional requests and workflows the commands below run.\n\n| Command | Runs | What it does |\n| --- | --- | --- |\n| `npm run check` | `check.ts` | Verifies the connection and reports what your key may do |\n| `npm run sync` | `sync.ts incremental mirror.json` | Walks modifications since the saved checkpoint |\n| `npm run export` | `sync.ts full mirror.json` | Re-reads the whole configured window from the start |\n| `npm run billing:plan` | `billing.ts plan manifest.json` | Selects candidates and saves a manifest |\n| `npm run billing:dry-run` | `billing.ts dry-run manifest.json` | Rehearses the write from that manifest |\n| `npm run push:dry-run` | `push.ts dry-run masterdata.json` | Rehearses upserting your customers and projects by `externalId` |\n\n**Every one of those is read-only against the API.** The two commands that\nwrite, executing a billing run and pushing customers and projects, are covered\nin the README, together with the\nconfiguration: origin, key, period and explicit UTC offsets. The starter kit is\nversioned with `v1`.\n\nIndividual sources:\n[client.ts](/api/integrations/v1/starter-kit/client.ts),\n[shared.ts](/api/integrations/v1/starter-kit/shared.ts),\n[check.ts](/api/integrations/v1/starter-kit/check.ts),\n[billing.ts](/api/integrations/v1/starter-kit/billing.ts),\n[sync.ts](/api/integrations/v1/starter-kit/sync.ts),\n[push.ts](/api/integrations/v1/starter-kit/push.ts).\n\n### Keys stay on your backend\n\n**Call this API from your backend, never from a frontend:** a key embedded in\nfrontend code is readable by every visitor. No CORS headers are sent, so a\nfrontend on another origin cannot read responses anyway.\n\nThe exception is this page: \"Test Request\" sends your key to the same origin\nwithout persisting it. Those are real calls with your key's permissions: a\nbilling request changes your data, and every call counts against the rate\nlimits.\n\n## Authentication\n\nEvery request carries the key as a bearer token:\n\n```\nAuthorization: Bearer sk_tf_int_live_…\n```\n\nA key is `sk_tf_int_live_` followed by 43 random characters and a\n6-character checksum of those 43, 64 in total. **A key that fails its own\nchecksum is refused with the same `401` `unauthorized` an unknown key\ngets**, so a value truncated on its way into your configuration looks exactly\nlike a revoked one. If a key you just created answers `401`, check that all\n64 characters arrived before suspecting anything else.\n\n**The API is off until someone switches it on**, and that comes first: an\nadministrator turns it on under [**Einstellungen → Funktionen →\nIntegrations-API**](/settings/features). Until then the key\npage is not even listed in the menu, and **every request answers `401`\n`unauthorized`**, `/me` included, exactly as a bad key does. So if a new\nkey answers `401`, check this switch before suspecting the key. Switching the\nAPI off revokes nothing, so revoke a leaked key.\n\nKeys are then created under **Einstellungen → Integrations-API → API-Schlüssel**,\nby someone holding the API-key permission. **The key is shown exactly once, at\ncreation.** It is stored hashed, so nobody can recover it, not even support:\nrevoke a lost key and create another. Revocation takes effect immediately.\n\n**A key can also expire.** Read `key.expiresAt` from `GET /me` at startup\n(`null` means no expiry) and rotate ahead of it: an expired key fails exactly\nlike a revoked one, `401` `unauthorized`. The warning mail TimeFusion sends\nbefore the date goes to administrators, not to your integration.\n\n### Scopes\n\nA key belongs to an organization, not to a person, and carries its own scopes:\n\n| Scope | Grants |\n| --- | --- |\n| `bookings:read` | Read time bookings. |\n| `masterdata:read` | Read projects, tasks, customers and hourly rates. |\n| `customers:write` | Create and update customers, including archiving and reactivating them. Setting a customer's hourly rate also requires `money:read`. |\n| `projects:write` | Create and update projects, including deactivating and reactivating them. Setting a project's hourly rate also requires `money:read`. |\n| `rates:write` | Create and update hourly rates, including archiving and reactivating them. Every rate write also requires `money:read`. |\n| `users:read` | Read the organization's people, including their email addresses and hourly rates. |\n| `billing:read` | Read invoices. |\n| `billing:write` | Mark and unmark time bookings as billed. |\n| `money:read` | Include hourly rates and monetary amounts in read responses. Requires the organization's accounting feature to be enabled. |\n\n**Scopes are fixed when the key is created.** Adopting an endpoint whose scope\nyour key lacks means creating a new key and switching over to it.\n\nA missing or invalid key is `401` `unauthorized`; a valid key lacking the\nendpoint's scope is `403` `forbidden_scope`.\n\n### Your first call\n\n`GET /me` needs no scope. Call it before writing any other code: it reports the\norganization with its time zone and currency, whether it uses the monthly close,\nthe calling key and its scopes, whether money is visible, and how much of the\nrate-limit window is left.\n\n```bash\nexport TF_ORIGIN=\"https://app.timefusion.at\"\nexport TF_KEY=\"sk_tf_int_live_…\"\n\ncurl -s \"$TF_ORIGIN/api/integrations/v1/me\" -H \"Authorization: Bearer $TF_KEY\"\n```\n\n```json\n{\n  \"organization\": {\n    \"id\": \"aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa\",\n    \"name\": \"Muster GmbH\",\n    \"currency\": \"EUR\",\n    \"timeZone\": \"Europe/Vienna\",\n    \"monthlyCloseStartMonth\": \"2026-07\",\n    \"readyForBillingEnabled\": true\n  },\n  \"key\": {\n    \"id\": \"44444444-4444-4444-8444-444444444444\",\n    \"name\": \"Nori Abrechnung\",\n    \"prefix\": \"sk_tf_int_live_a1b2\",\n    \"scopes\": [\n      \"bookings:read\",\n      \"billing:read\",\n      \"billing:write\",\n      \"money:read\"\n    ],\n    \"createdAt\": \"2026-08-01T10:00:00.000Z\",\n    \"expiresAt\": null\n  },\n  \"money\": {\n    \"visible\": true,\n    \"unavailableReason\": null\n  },\n  \"rateLimit\": {\n    \"limit\": 120,\n    \"remaining\": 119,\n    \"resetAt\": \"2026-08-14T07:31:00.000Z\"\n  }\n}\n```\n\n**Read `money.visible` at startup.** When it is `false`, every `rate` and\n`amount` in every response will be `null`, and `money.unavailableReason`\nsays which of the two gates closed:\n\n- `missing_scope`: This key was not granted `money:read`. Scopes are fixed when a key is created, so this one has to be replaced rather than widened.\n- `accounting_disabled`: The organization does not have the accounting feature enabled, which an administrator has to switch on.\n\nOnly the first failing gate is reported, so fixing one can reveal the other.\n\n`key.id` comes back as `billing.by.id` on every booking this key marks as\nbilled, so \"did *my* integration bill this?\" is an id comparison.\n\n## Reading data\n\nWhat a booking and the records around it say, for an integration that invoices\nand for one that only mirrors. [Billing workflow](#description/billing-workflow) builds on it.\n\n### Reading the numbers\n\n#### Time fields\n\n- **`kind`.** `tracked` is a real begin/end range; `plain` is a duration-only \"plain hours\" entry with no meaningful `endAt`.\n- **`endAt`.** Booking end in UTC. Null for a still-running booking, and for `plain` entries, which carry no end time. Use the duration fields to handle both booking kinds.\n- **`durationMinutes`.** Duration in whole minutes. Null means a tracked booking is still running; such bookings are available only through `GET /time-bookings/{id}` and excluded from lists. Do not sum null as zero. Not rounded by any organization rounding rule; this is the raw value.\n- **`durationHours`.** Duration in decimal hours, rounded to 3 decimals, with no organization rounding rule applied: 90 minutes is `1.5`. Use it to show hours instead of dividing `durationMinutes` yourself; amounts are computed from whole minutes, not from this field. A `tracked` booking computes it from the exact begin/end range, so it can differ slightly from `durationMinutes / 60`; for a `plain` entry it is exactly that. Null while a booking is still running, `0` when the end is before the start.\n- **Raw durations are why your totals will not match the PDF.** The\n  project-accounting PDF rounds; the API does not.\n\n#### Money fields\n\n- **`pricing`** (`billable`, `rate`, `amount`) says whether and how much the\n  work can be charged; **`billing`** (`billed`, `at`, `by`,\n  `invoice.number`) says whether an invoice already carries it. Both objects are\n  always present; their fields can be null. **The nesting is in the response\n  only**: the query filters and the billing request body are flat.\n- **`pricing.amount`.** `pricing.rate.amount × durationMinutes / 60`, rounded to 2 decimals. Null when the booking is not billable, still running, unpriced, or the key may not see money; never `0` for any of those. Summing it over a set of bookings matches the in-app project report to the cent, unless the report has its time-rounding option on.\n- **Every amount is in `organization.currency`** from `GET /me`. **Read it; do\n  not hard-code `EUR`.**\n- **`pricing.rate.frozen`.** True when the booking bills the snapshot frozen at billing time, so a later edit to the underlying rate cannot change it. False means the rate is resolved live and can still move.\n\n#### Invoice totals\n\n**`GET /invoices` reports time bookings and project expenses separately**, so an\nexpected difference can be told from a defect. The expense lines themselves are\nnot exposed in v1.\n\n- **`timeBookingAmount`.** Sum of the linked time bookings' amounts in the organization's currency, each rounded to 2 decimals first, so it matches what you get by summing a `GET /time-bookings` page. Null when the key may not see money, and null — never `0` — when the invoice has linked bookings but none of them could be priced. An invoice with no linked bookings reports `0`. Excludes unpriced bookings, and excludes the project expenses on the same invoice — those are `expenseCount` and `expenseAmount`.\n- **`timeBookingAmountComplete`.** True when every linked booking has a known amount (including an empty invoice), false for partial or entirely unpriced totals, null when money is hidden.\n- **`unpricedTimeBookingCount`.** How many linked time bookings are left out of `timeBookingAmount` because they have no amount: not billable, no rate, or no duration. A booking priced at exactly `0` is priced and not counted here. Null when the key may not see money.\n- **`expenseAmount`.** Sum of the linked project expenses in the organization's currency, rounded to 2 decimals, or null when the key may not see money. An invoice with no expenses reports `0` — unlike `timeBookingAmount`, there is no unpriced case here: an expense carries its own stored amount. Do **not** add it to `timeBookingAmount` and treat the result as the customer's invoice total; that document can carry tax, discounts and lines TimeFusion never saw.\n\nWhen money is visible, an invoice page may link at most\n10000 time bookings; a larger page is `400`, never partial\ntotals, so reduce `limit`. Total a single invoice over that bound by paging\n`GET /time-bookings?invoiceNumber=…` without dates, reconciling again if\nconcurrent billing changed its bookings. Pages with money hidden have no such\nbound.\n\n### The booking period\n\n`from` and `to` on `GET /time-bookings` are **required** and may span at most 366 days. A date without\na time means the whole UTC day, so `to=2026-08-31` includes the 31st. Timestamps\nwithout an offset are also interpreted as UTC; explicit offsets are respected.\n\n**`?invoiceNumber=` and `?id=` are the exceptions:** each bounds the result by\nitself, so both boundaries become optional and a lone one is open-ended. When\nboth boundaries are sent anyway, the span cap applies. `?id=`, repeated once per\nbooking, reads back the set a billing write echoed.\n\n**A date-only period is a UTC month, not a local one.** For an organization on\nUTC+2 in summer, `from=2026-08-01&to=2026-08-31` misses work recorded between\n00:00 and 02:00 local time on August 1, which is still July in UTC, and picks up\nthe same two hours of September 1 instead. To match the local calendar month,\nsend the boundaries with the offset that applied on those dates:\n\n```\nfrom=2026-08-01T00:00:00%2B02:00&to=2026-08-31T23:59:59.999%2B02:00\n```\n\nDerive the offset from `organization.timeZone` (an IANA zone,\n`Europe/Vienna` today) **and the date**: it moves with daylight\nsaving time, so a hard-coded `+02:00` is wrong for half the year.\n\nA completed `tracked` booking belongs to the period of its `endAt`, a `plain`\none to that of its `beginAt`, so a shift from August 31 to September 1 is\nreturned once, in September, at full duration. Both boundaries are inclusive:\ndate-only windows can run August 1–31 then September 1–30; timestamp windows\nmust not share an endpoint. The in-app report assigns cross-boundary bookings\ndifferently, a second reason period totals can differ.\n\n### Finalized bookings\n\nWhether the booking's recorded time is locked against editing by a period lock: the organization's change lock, or the owner's monthly-close lock when that workflow is enabled and the owner is not `monthlyCloseExempt`. Evaluated on `beginAt` in the organization's time zone (`organization.timeZone` on `GET /me`, Europe/Vienna today), for plain and running bookings too. Independent of `pricing.billable` and `billing.billed`. Current state, not a guarantee: reopening a period turns this back to `false`, and the booking becomes editable again unless it is billed or marked ready for billing (`billing.readyForBilling`), separate locks this field does not report.\n\nFilter with `?finalized=true` or `?finalized=false`, or omit it for both. The\nperiod filter places a completed tracked booking by its `endAt` and the lock by\nits `beginAt`, so a shift can appear in September's list and be finalized by an\nAugust lock. When fetching by ID, also require `durationMinutes !== null` before\ninvoicing; do not require `endAt`, which plain-hours bookings lack.\n\n**Whether anything can finalize depends on the organization.** One that runs\nneither lock finalizes nothing. The monthly-close half is on `GET /me`:\n\n- **While `organization.monthlyCloseStartMonth` is `null`,** nobody takes part\n  in the monthly close, no booking is ever report-locked, and `finalized` comes\n  from the change lock alone. Each person's `monthlyCloseExempt` on\n  `GET /users` is then moot, whatever it says.\n- **Once it is set,** a confirmed month locks its bookings, and the lock is not\n  bounded below: months before the start month have no report of their own, but\n  once a month from the start month on is confirmed, an earlier booking is\n  finalized with it.\n- **`monthlyCloseExempt: true` explains work that never finalizes:** the\n  monthly close skips that person, though the organization's change lock still\n  applies.\n\nThe change-lock half is not published; settle it with your customer.\n\n### The booking analysis\n\nEvery booking carries an `analysis` object, never null. Read `analysis.status`\non either a list item or a single booking:\n\n| Status | Meaning |\n| --- | --- |\n| `notApplicable` | Plain-hour entry; excluded from analysis. |\n| `pending` | No stored analysis timestamp; not a successful check or a promise that calculation is queued. |\n| `ok` | Analysis completed without a booking error. |\n| `error` | Analysis completed with a booking error. |\n\nShow `analysis.errorMessage` to the user; branch on `analysis.errorCode`.\nMessages are English and may be reworded. The message is null unless status is\n`error`. `analysis.calculatedAt` is the UTC timestamp of the stored\ncalculation; reading the API does not recalculate it.\n\n| Error code | `analysis.errorMessage` |\n| --- | --- |\n| `outsideOfBoundsPartial` | Booking lies partly outside recorded clock-in/clock-out periods. |\n| `outsideOfBounds` | Booking lies outside all recorded clock-in/clock-out periods. |\n| `overlapWithOther` | Booking overlaps another booking. |\n| `missingEnd` | Booking has no end time. |\n| `endBeforeBegin` | Booking ends before it starts. |\n\n`analysisStatus=` filters the list by the same rule: `analysisStatus=error` for\nthe bookings needing correction, `pending` for the ones with no stored result,\nor the parameter repeated to match any of several.\n\n**The billing write requires a clean analysis**: it refuses a booking whose\nstatus is `error` as `analysis_error`, and one still `pending` after its\nrecalculation as `not_analyzed`. [Selecting the work](#description/selecting-the-work) filters on\nthe two clean statuses, `ok` and `notApplicable`.\n\n### Resolving master data\n\n`GET /projects`, `GET /tasks`, `GET /customers` and `GET /rates`\n(`masterdata:read`) and `GET /users` (`users:read`, because it publishes\nemail addresses), each with a `/{id}` sibling, page like every other list.\nCustomers, projects and rates also answer `GET /external/{externalId}`, the\nrecord linked to your own id. A booking already embeds\n`project` (with `projectNumber`), `task` and `user` references; use these\nendpoints for what those lack, such as a project's customer, a stored\nrate or an employee's email, or to mirror master data ahead of the bookings\n([Master data in the same loop](#description/master-data-in-the-same-loop)).\n\n#### Projects, tasks and customers\n\n- **Deactivated and archived records are returned, flagged, not hidden.** A\n  project deactivated in September still has to resolve on August's bookings.\n  Filter with `?deactivated=false` (projects) or `?archived=false` (customers)\n  for only the live ones.\n- **Filters:** projects by `customerId`, tasks by `projectId` and by `kind`.\n  All four lists take `?id=`, and customers, projects and rates\n  `?externalId=`, each repeated to match any of several.\n- **`rate` on a task, project, customer or person is the rate stored there;** a\n  booking bills its `pricing.rate`, and `pricing.rate.source` names the level\n  that supplied it.\n- **A project's budget is `plannedHours`; the hours spent against it are\n  `GET /projects/{id}/hours`** (`bookings:read`): spent, billable, ready for\n  billing and billed, summed from the bookings when you ask. They are not on the\n  project itself, so recording time moves neither its `updatedAt` nor its\n  `ETag`. The app's *Stundenübersicht* on the project splits the hours the\n  same way, but as of its last *Stunden neu berechnen*, so it can lag behind\n  this endpoint, and it rounds only its totals where this endpoint rounds each\n  booking as `durationHours` does: the two can differ by a few thousandths of\n  an hour, and by a booking whose end lies before its start, which counts 0\n  here.\n\n#### Hourly rates\n\n- **`GET /rates` and `GET /rates/{id}` resolve every `rate.id` this API\n  returns.** The list is every stored hourly rate of the organization, whatever\n  uses it, including a rate assigned only to a person. Store the `id` and map\n  it to your own price list once; it survives a rename, where matching on\n  `name` and `amount` does not.\n- **`amount` is null when the key may not see money;** `id`, `name`,\n  `description`, `archived`, `isOrganizationDefault` and `updatedAt` stay\n  visible. This is the one place an amount is present-and-null rather than the\n  whole object being withheld, because the catalog exists to resolve ids and\n  names, and a name is not money.\n- **Archived rates are returned and flagged**, like deactivated projects and\n  archived customers; filter with `?archived=false` for only the assignable\n  ones.\n- **`isOrganizationDefault`** marks the rate the organization bills with when no\n  other level of the chain is set; no entry carries it when the organization has\n  no default. Changing the default writes the organization, not a rate, so the\n  flag flips without any entry's `updatedAt` moving: re-read `/rates` whole\n  rather than walking it by modification if you mirror this flag.\n- **A rate nothing uses can be deleted,** and then it is simply absent from\n  `GET /rates`, with no tombstone. A rate still assigned to anything cannot be\n  deleted, only archived, so every `rate.id` the API returns resolves here.\n\n#### The task tree\n\n- **Tasks may form a tree.** Every task's `path` runs from the root down and\n  **ends with the task itself**, so a breadcrumb renders from it alone; a task\n  outside any tree is a path of one. `parentTaskId` rebuilds the whole tree.\n- **Read `bookable`, not the position in the tree, and treat `kind` as a\n  label, not an enum.**\n\n#### People\n\n- **`GET /users` is where the employee mapping comes from:** match each\n  booking's `user.id` against your own records before the first sync, including\n  people with no bookings in your window.\n- **`email` can be `null`.** A member an administrator maintains has a\n  generated placeholder address with no mailbox behind it, so it is withheld.\n- **`rate` is the person's own stored hourly rate,** the level just above the\n  organization default in the chain that prices a booking. Null without\n  `money:read`, like every other amount, and not necessarily the rate any\n  given booking bills.\n- **Deactivated people are returned and flagged too**; filter with\n  `?deactivated=false` for the live roster.\n- **`monthlyCloseExempt`** says the monthly close skips this person; what that\n  means for `finalized` is under [Finalized bookings](#description/finalized-bookings).\n\n## Billing workflow\n\nAn invoicing run, once per period. It builds on [Reading data](#description/reading-data),\nwhich explains the filters it selects with; see\n[Billing & Invoices](#tag/billing-invoices) for every parameter and response.\n\n1. **Select the work.** Walk `GET /time-bookings` over one period and save the\n   exact booking IDs you intend to invoice. [Selecting the work](#description/selecting-the-work).\n2. **Rehearse.** Post the billing request with `\"dryRun\": true`. It answers\n   exactly as the real call would and writes nothing. [Try it first: `dryRun`](#description/try-it-first).\n3. **Bill.** Create the invoice in your own system from the saved IDs, then send\n   the same request with `\"dryRun\": false` and an `Idempotency-Key`.\n   [Execute after the rehearsal](#description/execute-after-the-rehearsal).\n4. **Reconcile.** The response echoes every id and the counts to check them\n   against. [Reconciling the write](#description/reconciling-the-write).\n5. **Recover.** A response you never saw is retried with the same key, never\n   reselected. [Retrying safely: `Idempotency-Key`](#description/retrying-safely).\n\n**Billing locks the booking for the employee who recorded it**\n([Billing freezes the booking for the employee](#description/billing-freezes-the-booking-for-the-employee)), so rehearse\nwhile you are building.\n\n### Selecting the work\n\nThe work that is ready to invoice:\n\n```bash\ncurl -s -G \"$TF_ORIGIN/api/integrations/v1/time-bookings\" \\\n  -H \"Authorization: Bearer $TF_KEY\" \\\n  -d from=2026-08-01 -d to=2026-08-31 \\\n  -d billable=true -d billed=false -d finalized=true \\\n  -d analysisStatus=ok -d analysisStatus=notApplicable\n```\n\n```json\n{\n  \"data\": [\n    {\n      \"id\": \"55555555-5555-4555-8555-555555555555\",\n      \"kind\": \"tracked\",\n      \"beginAt\": \"2026-08-14T07:30:00.000Z\",\n      \"endAt\": \"2026-08-14T09:00:00.000Z\",\n      \"durationMinutes\": 90,\n      \"durationHours\": 1.5,\n      \"analysis\": {\n        \"status\": \"ok\",\n        \"errorMessage\": null,\n        \"errorCode\": null,\n        \"calculatedAt\": \"2026-08-14T09:05:00.000Z\"\n      },\n      \"comment\": \"Implement invoice export\",\n      \"finalized\": true,\n      \"pricing\": {\n        \"billable\": true,\n        \"rate\": {\n          \"id\": \"aa000001-0000-4000-8000-000000000001\",\n          \"amount\": 120,\n          \"name\": \"Standard\",\n          \"source\": \"task\",\n          \"frozen\": false\n        },\n        \"amount\": 180\n      },\n      \"billing\": {\n        \"billed\": false,\n        \"at\": null,\n        \"by\": null,\n        \"invoice\": null,\n        \"readyForBilling\": {\n          \"at\": \"2026-08-31T15:40:00.000Z\",\n          \"by\": {\n            \"id\": \"bb000001-0000-4000-8000-000000000001\",\n            \"name\": \"Robin Example\"\n          }\n        }\n      },\n      \"user\": {\n        \"id\": \"66666666-6666-4666-8666-666666666666\",\n        \"name\": \"Alex Example\"\n      },\n      \"project\": {\n        \"id\": \"77777777-7777-4777-8777-777777777777\",\n        \"name\": \"Customer portal\",\n        \"projectNumber\": \"P-2026-001\"\n      },\n      \"task\": {\n        \"id\": \"33333333-3333-4333-8333-333333333333\",\n        \"name\": \"Implementation\",\n        \"path\": [\n          {\n            \"id\": \"11111111-1111-4111-8111-111111111111\",\n            \"name\": \"Engineering\",\n            \"bookable\": false,\n            \"kind\": \"subArea\",\n            \"order\": 1\n          },\n          {\n            \"id\": \"22222222-2222-4222-8222-222222222222\",\n            \"name\": \"Development\",\n            \"bookable\": false,\n            \"kind\": \"serviceArea\",\n            \"order\": 1\n          },\n          {\n            \"id\": \"33333333-3333-4333-8333-333333333333\",\n            \"name\": \"Implementation\",\n            \"bookable\": true,\n            \"kind\": \"activity\",\n            \"order\": 1\n          }\n        ]\n      },\n      \"updatedAt\": \"2026-08-14T09:05:00.000Z\",\n      \"billingRevision\": null\n    }\n  ],\n  \"pagination\": {\n    \"nextCursor\": \"eyJ2IjoxLCJzIjoidGltZS1ib29raW5ncyIs…\",\n    \"hasMore\": true\n  }\n}\n```\n\nFour filters, one selection: `billable=true` is work the customer pays for,\n`billed=false` is work no invoice carries yet, `finalized=true` is work\nwhose recorded time is locked and will not move underneath you, and the two\n`analysisStatus` values are bookings the analysis found clean or never\nanalyzes (plain hours). Everything on the page is a candidate, so there is\nnothing left to filter client-side.\n\n**The write refuses a booking whose analysis is `pending` or `error`**\n(`not_analyzed`, `analysis_error` under [Reconciling the write](#description/reconciling-the-write)), as the app\ndoes ([The booking analysis](#description/the-booking-analysis)). Do not leave them out silently, though: a later period's selection never\npicks them up again. Read the same selection with\n`analysisStatus=pending&analysisStatus=error` and report what stays behind. A\n`pending` booking is often only stale: the billing write recalculates it, so a\ndry run tells you whether it is actually clean.\n\nPage through with the cursor ([Pagination](#description/pagination)). Narrow the pull with\n`customerId`, `projectId`, `taskId` or `userId` to invoice per customer or\nproject rather than per period; to send several, repeat the parameter\n([Query parameters](#description/query-parameters)).\n\nFiltering on `finalized` is a **policy choice**, not a requirement: an\norganization that runs no change lock and no monthly close finalizes nothing, so\nthere `finalized=true` returns nothing and `billable=true&billed=false` is the\nwhole selection. [Finalized bookings](#description/finalized-bookings) says how to tell. If the\norganization releases work with the ready-for-billing mark, add\n`readyForBilling=true` ([Ready for billing: the project admin's release](#description/ready-for-billing-the-project-admins-release)).\n\n### Try it first: `dryRun`\n\nWhile you are building, send `\"dryRun\": true`:\n\n```bash\ncurl -s \"$TF_ORIGIN/api/integrations/v1/time-bookings/billing\" \\\n  -H \"Authorization: Bearer $TF_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n  \"bookingIds\": [\n    \"55555555-5555-4555-8555-555555555555\"\n  ],\n  \"billed\": true,\n  \"invoiceNumber\": \"RE-2026-0815\",\n  \"dryRun\": true\n}'\n```\n\nValidate the request and report what it would do, without writing anything. Every rejection a real call can produce is decided before any write, so a dry run returns the same `200`/`400`/`409`/`422` and the same `details[]` a real call would — `changed` then counts the bookings that *would* move. Like a real mark, a dry run first recalculates a missing booking analysis for the named bookings, so `analysis` and `updatedAt` (and the list `ETag`) can move on those bookings. Use it to exercise this endpoint safely: it is the only way to test a billing integration without freezing real bookings for the employees who recorded them. No invoice is created, so `invoice` is null for a number that does not exist yet. It works on an unmark too, needs\n`billing:write` and counts against the write rate limit.\n\n**Response `dryRun`.** Whether this response describes a simulation. `false` means the bookings were written; `true` means nothing was, and `changed` counts what a real call would have moved. Assert on it before recording an invoicing run as done.\n\nUnknown body properties are rejected, so a misspelled `dryrun` cannot silently\nbecome a real write.\n\n### Execute after the rehearsal\n\nCreate the invoice in your external system from the exact saved booking IDs,\nthen post the real write. This call changes real bookings:\n\n```bash\ncurl -s \"$TF_ORIGIN/api/integrations/v1/time-bookings/billing\" \\\n  -H \"Authorization: Bearer $TF_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Idempotency-Key: 3f8c1b9e-2d47-4a1e-9a1b-5f6f0f7c2a10\" \\\n  -d '{\n  \"bookingIds\": [\n    \"55555555-5555-4555-8555-555555555555\"\n  ],\n  \"billed\": true,\n  \"invoiceNumber\": \"RE-2026-0815\",\n  \"dryRun\": false\n}'\n```\n\nSend the `Idempotency-Key` you saved with the batch ([Retrying safely: `Idempotency-Key`](#description/retrying-safely)). It is\noptional, and the dry run must not carry one.\n\n```json\n{\n  \"invoice\": {\n    \"id\": \"99999999-9999-4999-8999-999999999999\",\n    \"number\": \"RE-2026-0815\"\n  },\n  \"changed\": 1,\n  \"unchanged\": 0,\n  \"bookingIds\": [\n    \"55555555-5555-4555-8555-555555555555\"\n  ],\n  \"dryRun\": false\n}\n```\n\nAn invoice with that number is created if it does not exist yet; `invoice.id`\nfetches it again later.\n\n### Reconciling the write\n\nOn a `200`, `changed + unchanged === bookingIds.length` always holds, and the\nresponse's own `bookingIds` echoes every id now in the target state, in request\norder, so you never have to re-read to confirm. `unchanged` counts bookings\nalready in the target state under the same invoice number.\n\nSend at most 500 ids per request, with no duplicates: a repeated id is\nrejected rather than collapsed.\n\n**Re-pointing is rejected.** A booking already billed under a different invoice\nnumber produces `422` with `already_billed_elsewhere`. Unmark it first\n(`billed: false`, no `invoiceNumber`) if you intend to move it.\n\nThe write is all-or-nothing. One id that cannot be billed rejects the whole\nrequest with `422` `validation_failed` and writes nothing; `details[]` names\nevery offending id with its own code:\n\n| `details[].code` | `details[].message` |\n| --- | --- |\n| `not_found` | No such booking. |\n| `not_billable` | The booking is not marked billable, so it cannot be put on an invoice. |\n| `not_completed` | The booking is still running and has no end time yet. |\n| `not_analyzed` | The booking has no booking analysis yet, so it cannot be checked for errors. Retry later; the analysis is recalculated on demand. |\n| `analysis_error` | The booking's analysis found an error, so it cannot be billed. The booking's `analysis.errorCode` names it; fix the booking first. |\n| `already_billed_elsewhere` | The booking is already billed under a different invoice number. Unmark it first — it is never silently re-pointed. |\n| `duplicate_id` | This booking id appears more than once in `bookingIds`. |\n\n### Retrying safely: `Idempotency-Key`\n\nOptional: 1–128 printable ASCII characters, case-sensitive. Send a unique key — a UUID is ideal — to make this write safely retryable: if you never see the response, send the **identical** request with the **same** key and the original result is returned instead of billing a second time. Keys are scoped to the organization and operation; any currently authorized API key in that organization may replay the result, even after the original API key is rotated or deleted. Receipts are retained for 7 days from execution and describe that original execution, not the bookings' current state. A replay carries `Idempotency-Replayed: true`. The same key with a different body is `409 idempotency_key_reused`; generate a new key when the intent changes. Cannot be combined with `dryRun: true`. Without this header nothing changes: identical marks still converge, but only absent intervening changes.\n\nGenerate one random key per batch, never one derived from an invoice number\n(every integration in the organization shares one key space), and save it with\nthe exact selection **before** sending. Retry that saved request; never\nreselect.\n\n**No key, or the receipt has expired?** Check\n`GET /time-bookings?invoiceNumber=…` before replaying the exact request. Never\nunmark or re-point automatically; a concurrent unmark or rebill needs\ninvestigation. Earlier batches can already be billed when a later one failed.\n\n### Billing freezes the booking for the employee\n\n**Once a booking is marked as billed, the person who recorded it can no longer\nedit it in the app.** Unmarking it (`billed: false`, through the API or in the\napp) releases only that billing lock: the organization's change lock and the\nmonthly-close lock still apply. Neither changes `finalized`. Do not mark\nbookings speculatively.\n\n### Ready for billing: the project admin's release\n\nA project admin can mark bookings **Abrechnungsbereit** to hand them to\naccounting.\n\nThe project admin's release of this booking to accounting (**Abrechnungsbereit** in the app), or null when it is not marked. A marked booking is locked against editing in the app until the mark is lifted, and the mark stays after billing, so a billed booking can carry it too. Filter with `?readyForBilling=`, and require it on the billing write with `requireReadyForBilling`. Whether the organization uses the mark is `organization.readyForBillingEnabled` on `GET /me`.\n\nIf your process invoices only released work, add `readyForBilling=true` to the\nselection and send `requireReadyForBilling: true` on the write\n([Billing against reviewed state](#description/billing-against-reviewed-state)). The mark is not `finalized`:\none is the project's release, the other the period locks, and an organization\ncan use either, both or neither.\n\n### Billing against reviewed state\n\nThree optional preconditions refuse the write when a booking changed between\nyour review and the write. They are independent: send any combination.\n\n| Precondition | Send | Needs | Catches |\n| --- | --- | --- | --- |\n| Period protection | `requireFinalized: true`, whenever you bill finalized bookings | `billing:write` only | a period reopened after your review |\n| Release | `requireReadyForBilling: true`, whenever you select with `readyForBilling=true` | `billing:write` only | a ready-for-billing mark lifted after your review |\n| Reviewed inputs (opt-in pilot) | `expectedBillingRevisions` | also `bookings:read` and visible money | a new rate, another task or any other changed billing input |\n\nVisible money means `money:read` with the organization's accounting feature\nswitched on; `money.visible` on `GET /me` says whether both are.\n\n- **`requireFinalized`.** Refuse the write unless every named booking is `finalized` in the billing transaction snapshot. Valid only when `billed` is true; omitted means false. A booking that is not finalized rejects the whole request with `409` `billing_precondition_failed`, naming that id in `details[]` as `not_finalized`. A period may reopen after that snapshot, including before billing commits; reopening does not undo billing.\n- **`requireReadyForBilling`.** Refuse the write unless every named booking carries a ready-for-billing mark (`billing.readyForBilling` not null) in the billing transaction snapshot. Valid only when `billed` is true; omitted means false. A booking without the mark rejects the whole request with `409` `billing_precondition_failed`, naming that id in `details[]` as `not_ready_for_billing`. Every named booking is checked, including one already billed under the same invoice. For an organization whose process requires the project admin's release before invoicing; whether the organization uses the mark at all is `organization.readyForBillingEnabled` on `GET /me`. Without this option billing does not require the mark, as in the app.\n- **`expectedBillingRevisions`.** The `billingRevision` you reviewed, keyed by booking id. Send exactly one non-empty token for every id in `bookingIds` and no others. Each token is re-checked against the billing transaction snapshot, so a booking whose inputs moved before that snapshot rejects the whole request with `409` `billing_precondition_failed`, naming that id in `details[]` as `stale_revision`, without disclosing current values. Validation and rate capture use the same snapshot; later rate changes do not change the captured rate. Requires `bookings:read` and visible money: `billing:write` grants neither, so a key that bills blind cannot send this.\n- **`billingRevision` on each booking.** Opaque token that changes whenever this booking's billing inputs change. Save it with the booking you reviewed and send it back in `expectedBillingRevisions`; the write is refused if any input moved in the meantime. Inputs: kind and effective duration, `pricing.billable`, `billing.billed` and its invoice, the resolved rate and its snapshot, the assigned user, task, project and customer, `finalized`, and whether `billing.readyForBilling` is set (not when or by whom). The booking analysis is not an input: the billing write checks it directly. Display names are not inputs, so a rename does not change it. Null when the key may not see money or the server has no signing key configured; null means no review is possible, not that the booking cannot change.\n\n```json\n{\n  \"bookingIds\": [\n    \"55555555-5555-4555-8555-555555555555\"\n  ],\n  \"billed\": true,\n  \"invoiceNumber\": \"RE-2026-0815\",\n  \"dryRun\": false,\n  \"expectedBillingRevisions\": {\n    \"55555555-5555-4555-8555-555555555555\": \"br1.k1.…\"\n  },\n  \"requireFinalized\": true\n}\n```\n\nIf a precondition fails, re-review, save a new manifest under a new\n`Idempotency-Key`, and reconcile any external invoice already created from the\nold selection. **Never unmark or rebill automatically.** A retained receipt\nstill replays its original result, even once its tokens are stale.\n\n### Unmarking\n\nThe same endpoint with `billed: false` releases bookings again. Send **no**\n`invoiceNumber` — the bookings' current one is what gets cleared, and sending one\nis `400` `invoice_number_not_allowed`.\n\n```bash\ncurl -s \"$TF_ORIGIN/api/integrations/v1/time-bookings/billing\" \\\n  -H \"Authorization: Bearer $TF_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n  \"bookingIds\": [\n    \"55555555-5555-4555-8555-555555555555\"\n  ],\n  \"billed\": false,\n  \"dryRun\": false\n}'\n```\n\n```json\n{\n  \"invoice\": null,\n  \"changed\": 1,\n  \"unchanged\": 0,\n  \"bookingIds\": [\n    \"55555555-5555-4555-8555-555555555555\"\n  ],\n  \"dryRun\": false\n}\n```\n\n`invoice` is `null` on every unmark, because the request names no invoice.\n**The invoice record itself survives:** `GET /invoices` keeps listing it, with\n`timeBookingCount: 0` once its last booking is released, and marking against\nthat number again links to the same record.\n\n### Finding an invoice again\n\n`GET /invoices?number=RE-2026-0815` answers \"did that invoice land, and what is\non it?\" without walking the list: the number is unique within your organization.\n`GET /invoices/{id}` returns the same record by its TimeFusion id.\n\n**To check a whole billing run, repeat the parameter:**\n`?number=RE-2026-0815&number=RE-2026-0816`, up to\n20 per request, is answered on one page. References\nthat landed come back; the absent ones are the list you have to act on.\n\n`from` and `to` window the list on `createdAt`. They are **optional**, and\neither works alone as an open-ended bound, so a first sync can fetch every\ninvoice. Given together they may span at most 366 days.\n\nTo see what is *on* an invoice, use `GET /time-bookings?invoiceNumber=…` with\nno dates; the invoice endpoints report counts and totals, not the bookings.\n\nBefore reconciling a total, require `timeBookingAmountComplete === true`.\nIf it is false or null, do not report a complete match. [Invoice totals](#description/invoice-totals)\ncovers completeness and the separate expense total.\n\n## Keeping in sync\n\n**For invoicing, re-query the period.** It is bounded, and re-reading last month\nbefore closing it is intended. A mirror that must notice a *late* edit, such as\nMarch corrected in September, needs `sort=updatedAt`, below. The TypeScript\nsample on `GET /time-bookings` combines pagination, retries and conditional\nreads; [sync.ts](/api/integrations/v1/starter-kit/sync.ts) runs\nboth walks with durable file storage.\n\n### Finding late edits: `sort=updatedAt`\n\nOne day of modifications, whenever the work itself happened:\n\n```bash\ncurl -s -G \"$TF_ORIGIN/api/integrations/v1/time-bookings\" \\\n  -H \"Authorization: Bearer $TF_KEY\" \\\n  -d sort=updatedAt \\\n  -d from=2026-09-01T00:00:00Z -d to=2026-09-02T00:00:00Z\n```\n\n**`sort=updatedAt` makes `from` and `to` select by last modification**\ninstead of by period, oldest-modified first. Everything else holds: the window\nis required unless `?invoiceNumber=` or `?id=` bounds the list, a full window\nis capped at 366 days, running bookings are excluded, and the envelope is\nthe same.\n\n**A row timestamp is not a commit watermark:** a transaction can commit an\nearlier timestamp after you read a later one, and an edit moves a row to the end\nof the order, behind your cursor. So:\n\n- Upsert every returned record by ID, including ones already seen.\n- Save the highest timestamp only after the whole walk and its local writes\n  succeed; on failure, replay the window from its start.\n- Start the next run an overlap before that checkpoint. If nothing arrived, keep\n  the old checkpoint rather than fabricating one from your clock.\n- **No overlap makes this lossless.** Periodically reconcile every period you\n  retain, plus master data: a filtered walk cannot report a booking leaving its\n  filter, and there are no tombstones.\n\n**`updatedAt` covers the booking's own row only.** Internal bookkeeping, such\nas a recalculated analysis, also moves it, which is harmless over-reporting. But\nrenaming a project, adding a `projectNumber`, or renaming an ancestor task or an\nemployee changes what a booking returns **without moving its `updatedAt`**.\nKey your tables on ids and resolve names at render time, or re-read master data\non its own schedule ([Master data in the same loop](#description/master-data-in-the-same-loop)). A conditional re-read of a period\ndoes catch renames, because the `ETag` hashes the whole body.\n\n**Live prices move without `updatedAt` too.** Until a booking is billed\n(`pricing.rate.frozen: false`), its `pricing.rate` and `pricing.amount` are\nresolved from the rate chain on every read. Editing a rate, assigning or clearing\none on a task, project, customer or person, changing the organization default, or\nmoving a project to another customer can change them for many bookings at once,\nand none of those writes touches a booking row. Walking the master-data lists\ntells you such a change happened, not which bookings it repriced. Treat the\nprice of an unbilled booking as provisional and re-read the periods you invoice\nfrom before relying on it; a billed booking's rate is frozen and does not move.\n\n### Master data in the same loop\n\n`/projects`, `/tasks`, `/customers` and `/rates` take\n`?sort=createdAt|updatedAt`, with `createdAt` the default, and under\n`sort=updatedAt` the same `from`/`to` pair as the bookings, so one sync loop\ncan walk all four beside them:\n\n```bash\ncurl -s -G \"$TF_ORIGIN/api/integrations/v1/customers\" \\\n  -H \"Authorization: Bearer $TF_KEY\" \\\n  -d sort=updatedAt \\\n  -d from=2026-09-01T00:00:00Z -d to=2026-09-02T00:00:00Z\n```\n\nTwo differences from the booking walk. `from` and `to` are **optional** here\n— the lists are small enough that a first sync reads them whole — and there is\nno span cap, so one walk can cover a year of modifications, still paged by\n`limit` and `cursor`. A `to` earlier than `from` is\n`400 invalid_period`, as on bookings. They are\n**only valid with `sort=updatedAt`**: under the default the API answers\n`400 invalid_parameters` rather than ignoring them, because a caller who sent\nthem believes the walk is bounded.\n\n**Walk all four, not just the list you need:** `updatedAt` is the\nrecord's *own* write time. Renaming a customer leaves its projects' and those\nprojects' tasks' timestamps untouched; renaming an ancestor task leaves its\ndescendants' untouched; editing a rate moves the rate and not the task,\nproject or customer pointing at it. So a mirror that walks only the list it\nuses misses those renames, while one walking all four catches each change on\nthe list that owns the row. When in doubt, an `ETag` makes re-reading a list\nwhole one cheap request. The row-timestamp warning from above applies here too:\nresume with overlap. See [sync.ts](/api/integrations/v1/starter-kit/sync.ts)\nfor the four-list walk beside the booking one.\n\n### Polling cheaply\n\nEvery read except `GET /me` carries an `ETag`. Store it with the cached body\nand send it back on the next poll:\n\n```http\nGET /api/integrations/v1/time-bookings?from=2026-08-01&to=2026-08-31 HTTP/1.1\nAuthorization: Bearer sk_tf_int_live_…\nIf-None-Match: \"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\"\n```\n\nAn unchanged **page** answers `304 Not Modified` with no body: reuse the cached\nbody, including `pagination.nextCursor`, and keep walking. Without a cached\nbody, retry unconditionally.\n\n- **Check every page.** A 304 on page one says nothing about page two, and a\n  changed page can issue a new cursor URL that needs an unconditional fetch.\n- **Store one tag per request, not per resource**: it covers that exact body,\n  including its filters, its cursor and whether your key may see money. Treat it\n  as opaque.\n- **A malformed `If-None-Match` is ignored**: you get the full `200`.\n- **A `304` still counts against your rate limit.**\n\n### A deleted booking simply disappears\n\nThere are no tombstones or deletion events: a deleted booking is just absent\nfrom the next read, and `sort=updatedAt` cannot show it either. Billed bookings\ncannot be deleted, so nothing on your invoices vanishes. An unbilled booking\nmissing from a period may have been deleted, moved or edited mid-walk: finish\nthe walk and recheck its ID before removing it locally, never on absence from a\nfailed or partial walk.\n\n## Syncing master data\n\nAn ERP or CRM can keep TimeFusion's customers, projects and hourly rates in step\nwith its own: create and update them by **your own ids**, archive and deactivate\nthem, and read back what people changed in the app. Writing needs\n`customers:write`, `projects:write` or `rates:write` for the records you\npush, and reading the result back `masterdata:read`; scopes never imply each\nother. **Push in dependency order: rates, then customers, then projects**, since\na project names its customer and both name a rate\n([Your price list: rates by your own id](#description/your-price-list-rates-by-your-own-id)). See [Customers](#tag/customers),\n[Projects](#tag/projects) and [Hourly rates](#tag/hourly-rates) for every\nparameter.\n\nBookings, tasks and project members stay in the app, and nothing is deleted\nthrough the API: archive a customer or rate (`archived: true`) or deactivate a project\n(`deactivated: true`), which has the same effects as doing so in the app.\n\n### Linking by your own id: the upsert\n\n`PUT /customers/external/{externalId}` and `PUT /projects/external/{externalId}`\nare the sync write. They update the record linked to your id, or create one\nlinked to it (`201`, otherwise `200`), and answer with the record as\n`GET /{id}` returns it, plus its `ETag`:\n\n```http\nPUT /api/integrations/v1/projects/external/P-2026-001 HTTP/1.1\nAuthorization: Bearer sk_tf_int_live_…\nContent-Type: application/json\n\n{ \"name\": \"Customer portal\", \"projectNumber\": \"P-2026-001\", \"customerExternalId\": \"K-1001\" }\n```\n\n- **`externalId`** is 1 to 128 printable ASCII characters, case-sensitive,\n  unique per organization and resource. Records read back carry it, and\n  `?externalId=` finds them (`?id=` by TimeFusion's id). Only the API sets\n  it; the app shows it read-only.\n  `PATCH /{id}` with `externalId` links, re-links or (with `null`) unlinks a\n  record.\n- **Merge semantics.** An omitted field is left as it is; `null` clears a\n  nullable one. Send only the fields your system owns, so a field people\n  maintain in the app is never overwritten with a stale copy.\n- **A body that changes nothing writes nothing.** `updatedAt`, `updatedBy` and\n  the `ETag` stay put, so sending your whole list on every run is safe and\n  cheap, and does not show up as a change on the next `sort=updatedAt` walk.\n- **A project names its customer** by `customerId` or by\n  `customerExternalId`, the id your system linked it under; never both.\n  `null` in either removes it. Send the customers first. An unknown one is `422 customer_not_found`.\n- **A customer or project names its rate** by `rateId` or by\n  `rateExternalId`, the id your system linked the rate under; never both.\n  `null` in either clears it. Either needs `money:read` and the\n  organization's accounting feature (`403 money_unavailable` otherwise,\n  naming the field and the gate). An archived rate cannot be newly assigned.\n\n### Your price list: rates by your own id\n\n`PUT /rates/external/{externalId}` and `POST /rates/batch` keep TimeFusion's\nhourly rates in step with your price list, by the same rules as customers and\nprojects: the upsert, merge semantics, `If-Match`, batches. Customers and\nprojects then name a rate by `rateExternalId`, so your system never stores\nTimeFusion's rate ids. Push the rates first.\n\n```http\nPUT /api/integrations/v1/rates/external/PL-SEN HTTP/1.1\nAuthorization: Bearer sk_tf_int_live_…\nContent-Type: application/json\n\n{ \"name\": \"Senior Consulting\", \"amount\": 150 }\n```\n\n- **Every rate write needs money**: `rates:write`, `money:read` and the\n  organization's accounting feature, whatever the body sends. A rate is a\n  price.\n- **`amount` is live.** Changing it re-prices every booking not yet billed\n  that resolves to the rate, exactly as an edit in the app does, and moves\n  their `billingRevision`. Billed bookings keep the amount they were billed\n  at. Change a price from a date on by creating a new rate and moving the\n  customers and projects to it.\n- **Names are not unique.** `adoptExisting` links the one unlinked rate with\n  the same name, compared case-insensitively; two are `409 ambiguous_adoption`.\n  Without the flag, a rate of the same name is created beside it.\n- **Archiving** (`archived: true`) stops new assignments; records already\n  using the rate keep it. Which rate is the organization default is set in the\n  app, not here.\n\n### The first run: adopting what was typed in by hand\n\nRecords people created in the app are not linked to your ids yet, and an upsert\nwould create a duplicate beside each. Send `?adoptExisting=true` on the first\nrun (`adoptExisting: true` on a batch): when no record is linked to the\n`externalId`, an **unlinked** one is linked instead and the body applied to it.\n\n- **A customer is matched by name**, case-insensitively. Names are unique per\n  organization, so without the flag a taken name is `409 customer_name_taken`,\n  with the holder's id in `details[]`: link that one explicitly with\n  `PATCH /customers/{id}` and `externalId`, or choose another name. Older\n  customers can share a name in different cases; more than one unlinked match\n  is `409 ambiguous_adoption`, as for projects.\n- **A project is matched by `projectNumber`**, and only when exactly one\n  unlinked project carries it. Project numbers are not unique, so two candidates\n  are `409 ambiguous_adoption`, with both ids in `details[]`.\n- **A rate is matched by name**, case-insensitively, and only when exactly one\n  unlinked rate carries it.\n- A record already linked to another `externalId` is never adopted.\n\n`If-None-Match: *` makes an upsert create-only and `If-Match: *` update-only;\neither answers `412 precondition_failed` otherwise and writes nothing.\n\n### When an id changes in your system\n\nThe upsert never changes the `externalId` it is addressed by, and one id\nlinks one record at a time: setting it on a second record is\n`409 external_id_taken`. Move a link with `PATCH /{id}` instead.\n\n- **Your system renumbered a record.** `PATCH /{id}` with the new\n  `externalId`. Upserts under the old id now create a new record, so switch\n  them over in the same run.\n- **The link points at the wrong record.** `PATCH` the wrong one with\n  `externalId: null`, then `PATCH` the right one with the id. In that\n  order: the second fails while the first still holds it.\n- **Your system deleted a record.** Archive the customer or deactivate the\n  project, and keep the link if the id may come back; clear it with `null` if\n  your system reuses ids, so a later record under that id is created fresh.\n\nAn unlinked record is a candidate for `adoptExisting` again, so a rerun with\nthe flag may link it back.\n\n### Batches\n\n`POST /rates/batch`, `POST /customers/batch` and `POST /projects/batch` upsert up to\n100 items by `externalId` in one transaction, exactly as the\n`PUT` does each. A batch is **one request** against the write budget of\n20/minute, so an initial sync of thousands of records takes a few\ndozen requests.\n\n```http\nPOST /api/integrations/v1/customers/batch HTTP/1.1\nAuthorization: Bearer sk_tf_int_live_…\nContent-Type: application/json\n\n{ \"items\": [{ \"externalId\": \"K-1001\", \"name\": \"Example GmbH\" }], \"adoptExisting\": true, \"dryRun\": true }\n```\n\n- **All or nothing.** One refused item refuses the batch with\n  `422 batch_rejected`, listing **every** refused item in `details[]` by\n  `index` and `externalId`, with the code it would have met alone. Nothing is\n  written.\n- **`dryRun: true`** runs every check and reports each item's `outcome`\n  (`created`, `updated`, `unchanged`) and writes nothing. Its creates never\n  existed, so a project batch rehearsed before its customers are written cannot\n  name those customers yet.\n- **Items run in order and see each other**, and each `externalId` may appear\n  once. Each item answers with its `record` and `etag`; send the `etag`\n  back as the item's `ifMatch` next time.\n- `Idempotency-Key` replays a batch's exact answer, as on the billing write.\n  The upsert does not need one to be safe; `POST /rates`, `POST /customers`\n  and `POST /projects`, which create a second record when sent twice, do.\n\n### Not overwriting an edit made in the app\n\nWithout a precondition, the last write wins. To refuse a write when someone\nchanged the record since you read it, send the `ETag` you last saw, from\n`GET /{id}`, `GET /external/{externalId}` (the upsert's own URL, by your\nid) or your previous write, as `If-Match` on the upsert (`ifMatch` on a batch item). A mismatch is `412 precondition_failed` and writes nothing:\nre-read, merge, and send again.\n\n`PATCH /{id}` takes no entity tag: `If-Match` with one, or any\n`If-None-Match`, is `400` and writes nothing. Our hosting's CDN would\nanswer a successful conditional `PATCH` with a `412` of its own after the\nwrite ran, telling you a saved change failed. Update by your id with the upsert\nwhen the write must be conditional; give a record your id with an\nunconditional `PATCH` first if it has none.\n\nThe `ETag` covers the whole body, so renaming the record's customer or rate,\nor the person or key in `updatedBy`, also fails it without anyone touching the\nrecord itself. That is a spurious `412` you resolve by re-reading, never a lost\nupdate. A weak tag (`W/\"…\"`, as a compressed response carries it) counts as\nthe strong one it was. A malformed `If-Match` is `400`, unlike\n`If-None-Match` on a read: a write precondition the server ignored is the lost\nupdate it exists to prevent.\n\n### Recognising your own writes\n\nTo pick up edits made in the app, walk `sort=updatedAt` ([Master data in the same loop](#description/master-data-in-the-same-loop)). Your own writes come back on that walk too, and a sync that copies\nthem back into your system can ping-pong one change between the two forever.\n\nEvery rate, customer and project carries `createdBy` and `updatedBy`: a person\n(`type: \"user\"`) or an API key (`type: \"api_key\"`), `null` when unknown (records\nolder than these fields). **A row is your own write when `updatedBy.id` is\n`GET /me`'s `key.id` and its `updatedAt` is the one your write returned.**\nStore that `updatedAt` per record. Each half covers what the other cannot:\n\n- **`updatedAt` alone does not say who.** Any later write moves it, and the\n  walk returns it either way.\n- **`updatedBy` alone misses bookkeeping.** Reordering the list in the app\n  moves `updatedAt` and leaves `updatedBy` as it was, so with both checks\n  anything that happened after your write counts as a change. Treating such a\n  row as changed costs at most one write that changes nothing.\n- **The key id changes on rotation.** Rows your previous key wrote name that\n  key, so they read as someone else's until you write them again; keep the\n  previous key's id beside the current one for a run, or accept that one round.\n\nA write that changes nothing moves neither, so a sync that sends its whole list\nevery run stays quiet.\n\n### What an API-created project lacks\n\nA project created through the API has **no members and no tasks**: a key is not\na person who could be its first member. Someone with project management rights\nfinishes its setup in the app. Or create it from a template:\n`templateProjectId` copies the template's members with their project roles and\nits task tree, and its customer and (unless archived) its rate when the body\nnames none, as creating from a template in the app does. It applies when the\nrequest creates; an upsert that updates ignores it. A template this\norganization does not have is `422 template_not_found`.\n\n## API conventions\n\n### Query parameters\n\n**Several values? Repeat the parameter.** Repeated values are ORed, different\nparameters ANDed: `?projectId=a&projectId=b&userId=c` is \"either project, that\nperson\". It is the only multi-value form in v1. A comma is part of the value\n(`?invoiceNumber=RE-1,RE-2` is one reference), and bracket forms such as\n`?projectId[]=a` or `?projectId[0]=a`, the default in axios, jQuery, `qs` and\nPHP, are a `400` so they cannot silently drop a filter. Each parameter publishes\nits own value cap; duplicates are ignored. In \"Test Request\" on this page, add\none Query Parameters row per value, all with the same key, and enable each row.\n\n**Any other unknown query parameter is ignored**, so a misspelled filter\nwidens the read instead of failing it: check parameter names against the\noperation.\n\n### Pagination\n\nCursor-based on every list: pass `pagination.nextCursor` back as `cursor`\nuntil `hasMore` is false, keeping the filters unchanged. There is no total\ncount. `limit` defaults to 50 and is rejected, not clamped, above 200.\n\nLists are ordered oldest first, with the id as tiebreak: `(beginAt, id)` for\n`GET /time-bookings`, `(updatedAt, id)` under `sort=updatedAt`, and\n`(createdAt, id)` everywhere else. **Cursors are opaque and belong to one\nendpoint _and_ one ordering**: do not construct, parse or move one; a moved\ncursor is `invalid_cursor`. **A walk is not a snapshot**: rows can move while\nyou page ([Finding late edits: `sort=updatedAt`](#description/finding-late-edits)).\n\n### Rate limits\n\nPer key: **120 requests/minute** for reads (`GET` and `HEAD`), **20/minute** for\nwrites, using separate sliding 60-second windows. Responses that reach the\nper-key limiter carry `X-RateLimit-Limit`, `X-RateLimit-Remaining` and\n`X-RateLimit-Reset`; they are absent on the public docs routes, before\nauthentication succeeds, and while rate limiting is off or unavailable. Every\n`429` carries `Retry-After`, including one that throttles failed\nauthentication.\n\n**`X-RateLimit-Reset` is an epoch timestamp in seconds; `Retry-After` is a\ndelta in seconds.** Sleep for `Retry-After`. A `429` consumed no page: retry\nthe same URL with the same cursor.\n\n### Errors\n\nEvery error the API answers shares one envelope:\n\n```json\n{\n  \"error\": {\n    \"code\": \"validation_failed\",\n    \"message\": \"…\",\n    \"details\": [{ \"id\": \"…\", \"code\": \"already_billed_elsewhere\" }]\n  }\n}\n```\n\n**Branch on `error.code`, not on the status**: the code names the reason, one\ncode per reason. The full list:\n\n| Status | Code | What to do |\n| --- | --- | --- |\n| 400 | `invalid_parameters` | Fix the fields `details[]` names. |\n| 400 | `malformed_json` | Check the payload and the `Content-Type` header. |\n| 400 | `invalid_period` | Send a real calendar date, with `to` no earlier than `from`. |\n| 400 | `period_too_long` | Split the sync into narrower windows. |\n| 400 | `invalid_cursor` | Discard the cursor and restart the walk from the first page. |\n| 400 | `invoice_number_required` | Send an `invoiceNumber` with `billed: true`. |\n| 400 | `invoice_number_not_allowed` | Drop the `invoiceNumber` when unmarking. |\n| 400 | `invoice_page_too_large` | Lower `limit` on the list; a single invoice over the bound has no `limit` to reduce and is totaled through `GET /time-bookings?invoiceNumber=…` instead. See the amount limit above. |\n| 400 | `idempotency_key_not_allowed` | Drop the key from the dry run and send it with the execution. |\n| 401 | `unauthorized` | Send a valid key as `Authorization: Bearer sk_tf_int_live_…`. If a key that worked before answers `401`, have an administrator check that it was not revoked or expired and that the API is still switched on. |\n| 403 | `forbidden_scope` | Scopes are fixed at key creation, so issue a new key carrying the scope named in the message. |\n| 403 | `money_unavailable` | Drop the rate field, issue a new key carrying `money:read`, or have an administrator switch accounting on, as the reason says. A rate write has no field to drop: it always needs money. |\n| 404 | `not_found` | Check the path and the id. |\n| 405 | `method_not_allowed` | Use a method the `Allow` header lists. To retire a record, archive the customer or rate (`archived: true`) or deactivate the project (`deactivated: true`). |\n| 409 | `billing_precondition_failed` | Re-read and re-review the selection, reconcile any external invoice, then send a new request under a new key. |\n| 409 | `conflict` | **Retry**, after re-reading the bookings. |\n| 409 | `invoice_page_changed` | **Retry.** A request that is genuinely oversized then fails deterministically as `400 invoice_page_too_large`. |\n| 409 | `idempotency_key_reused` | Send the original request unchanged, or a new key for a new intent. See *Retrying safely*. |\n| 409 | `idempotency_request_in_progress` | **Retry** the identical request after a few seconds; it replays that request's result rather than executing twice. |\n| 409 | `customer_name_taken` | When it is the customer you meant, link it: set your `externalId` on it with `PATCH /customers/{id}`, or send the upsert again with `?adoptExisting=true` (`adoptExisting: true` on a batch). When it is a different customer, choose another name. |\n| 409 | `external_id_taken` | Clear it on the other record first (`PATCH` with `externalId: null`), or send a different one. `GET /customers?externalId=…`, `GET /projects?externalId=…` or `GET /rates?externalId=…` finds the holder. |\n| 409 | `ambiguous_adoption` | Link the right record explicitly with `PATCH /projects/{id}`, `PATCH /customers/{id}` or `PATCH /rates/{id}` and its `externalId`. |\n| 412 | `precondition_failed` | On a stale tag, re-read the record, reconcile the change, and send the write again with the new `ETag`. On `If-None-Match: *` the `externalId` is already linked, and on `If-Match: *` it is not: decide whether to update or create, and resend without the header. |\n| 422 | `validation_failed` | Use each `details[].code` to correct the request or booking state, then retry the whole batch. |\n| 422 | `batch_rejected` | Correct or drop the listed items, then send the whole batch again. |\n| 422 | `customer_not_found` | Check the reference; when the customer is new, create it first. |\n| 422 | `template_not_found` | Check the id, or create the project without a template. |\n| 422 | `rate_not_found` | Check the reference against `GET /rates`; when the rate is new, create it first (`PUT /rates/external/{externalId}`). |\n| 422 | `rate_archived` | Choose an active rate (`GET /rates?archived=false`). |\n| 429 | `rate_limited` | Wait for `Retry-After`, a delta in seconds, then retry. |\n| 500 | `internal_error` | Quote the `requestId` (or `X-Request-Id`) in a support request. |\n\n`details[]` appears on these codes only:\n\n- `invalid_parameters`: each entry names a `field`.\n- `validation_failed`: an `id` and a code from [Reconciling the write](#description/reconciling-the-write).\n- `billing_precondition_failed`: an `id` with `stale_revision`,\n  `not_finalized` or `not_ready_for_billing`.\n- `customer_name_taken`: the `id` of the customer holding the name, as\n  `existing_customer`.\n- `ambiguous_adoption`: the `id` of each candidate project, customer or\n  rate, as `adoption_candidate`.\n- `money_unavailable`: why, as `missing_scope` or `accounting_disabled`,\n  and the `field` that needs money (`rateId` or `rateExternalId`). No\n  `field` on a rate write, which needs money whatever it sends.\n- `batch_rejected`: each refused item by `index` and `externalId`, with the\n  `code` it would have answered alone and, where that code names records,\n  their `ids`.\n\nTheir schemas are `ValidationError` on a `400`, `BillingValidationError` on\nthe billing `422`, `BatchValidationError` on the batch `422`, and `Error`\neverywhere else.\n\n**Parse `code`, `details[].field`, `details[].id` and `details[].index`,\nnever `message`**: it is prose for a person and may be reworded in any\nrelease.\n\n**Every response carries `X-Request-Id`**, and a `500` repeats it as\n`error.requestId`. Log it; support needs it to find your request.\n\n**Every response also carries `X-Api-Version`**, this document's\n`info.version`. Compare it with the version your client was written against\nto notice a release without downloading `openapi.json`; the starter kit\nintegration's client warns on any difference.\n\n**A wrong HTTP method is a `405`.** `v1` answers `GET`, `POST`,\n`PATCH` and `PUT` (`HEAD` counts as a read). One of the four, or\n`DELETE`, on a path that does not take it is **`405` `method_not_allowed`**,\nwith `Allow` listing the methods the path does answer; nothing is deleted\nthrough the API. A path that exists under no method stays `404`\n`not_found`. Any other verb never reaches the API: the framework in front of\nit answers with an empty body, no `error.code`, and no `X-Request-Id` or\n`X-Api-Version`. Do not parse or retry it.\n\n### The app is German, the API is English\n\nYour counterpart verifies the result on a German screen. The words that matter:\n\n| API | In the app |\n| --- | --- |\n| `finalized` | durch Einträge-Sperre oder Monatsabschluss gesperrt |\n| `pricing.billable` | verrechenbar |\n| `pricing.rate` | Stundensatz |\n| `billing.billed` | abgerechnet |\n| `billing.at` / `billing.by` | wann und von wem abgerechnet |\n| `billing.invoice.number` | Rechnungsnummer |\n| `project` | Projekt |\n| `projectNumber` | Projektnummer |\n| `plannedHours` | geplante Stunden (Stundenbudget) |\n| `spentHours` | gebuchte Stunden (Stundenübersicht) |\n| `task` | Aufgabe (eines Projekts) |\n| `booking` | Buchung (auf eine Aufgabe eines Projekts) |\n| `comment` | Kommentar zur Buchung einer Aufgabe |\n| `customer` | Kunde |\n| `durationMinutes` | Dauer in Minuten |\n| `durationHours` | Dauer in Dezimalstunden |\n| `analysis.status` | Status der Buchungsanalyse |\n| `analysis.errorCode` | Fehler der Buchungsanalyse |\n| `analysis.errorMessage` | Fehlerbeschreibung (English) |\n| `analysis.calculatedAt` | Zeitpunkt der letzten Buchungsanalyse |\n| API key | API-Schlüssel, under **Einstellungen → Integrations-API → API-Schlüssel** |\n| The API being switched on | Integrations-API, under **Einstellungen → Funktionen** |\n\nThe screen to compare totals against is the project's **Abrechnung** view, with\nthe rounding caveat under [Reading the numbers](#description/reading-the-numbers).\n\n### Versioning\n\n`v1` is frozen: a breaking change gets a new path prefix, an additive one lands\nin `v1` ([Changes](#description/changes)). Ignore unknown fields rather than failing on them.\n\n**Vocabularies publish as strings, not enums.** `error.code`,\n`ApiKeyContext.scopes`, `Rate.source`, `analysis.status`,\n`analysis.errorCode`, `kind` and the per-entry `details[].code` each name\ntheir current values in the field's own description and may gain one in any\nrelease: branch on the values you know and keep a default case, which for an\n`error.code` means falling back to the HTTP status class. The values already\npublished are never removed or renamed. The one **closed** response enum is\n`BilledBy.type`, where the value selects the object's shape. A **request-only**\nenum may gain accepted values; removing or renaming one is breaking. These rules\nhold against every released contract.\n\n[`openapi.json`](/api/integrations/v1/openapi.json) is the contract as\n**OpenAPI 3.2**, public and generated from the schemas that validate each\nrequest. Generate your client from it with tooling that supports 3.2 (rewriting\nthe version declaration is not supported), and check it in so CI diffs it. The\nstarter kit uses plain HTTP and needs no generator.\n\n#### Lifecycle\n\n**`v1` keeps working when `v2` ships**, served beside it. A retirement is\nannounced **12 months** ahead, counted from the announcement. From then, every\n`v1` response carries a `Deprecation` header (RFC 9745) and a `Sunset` header\n(RFC 8594), the affected operations are `deprecated: true` in `openapi.json`,\nand your administrators are emailed. After the sunset date the path answers\n`410`. Nothing is deprecated today.\n\n### Changes\n\n`info.version` is semver over this contract:\n\n- **patch** — documentation only: a description reworded, an example corrected.\n- **minor** — anything additive: a new field, a new endpoint, a new optional\n  parameter, additional accepted request-only enum values, or a new value in a\n  published vocabulary such as a scope or an `error.code` (see [Versioning](#description/versioning)).\n- **major** — never in this document. A breaking change is `/api/v2`, a separate\n  document starting at `2.0.0`.\n\nThis document is `1.2.0`, newest release first.\n\n#### 1.2.0 (released 2026-10-15)\n\nAdditive, with one approved **tightening**.\n\n- **The billing write refuses a booking whose analysis found an error or that has none**, as the app has since 2026-10-10: a mark that names one is `422` with `analysis_error` or `not_analyzed` in `details[]`, and under the all-or-nothing rule the whole request with it. A request that billed such a booking before now fails; select with `analysisStatus=ok&analysisStatus=notApplicable` to avoid it. Before checking, a mark (dry runs included) recalculates a missing analysis of the named bookings, which can move their `updatedAt`. Unmarking and a booking already billed under the same invoice are not checked.\n- **Ready for billing:** a booking publishes `billing.readyForBilling` (`at`, `by`), the project admin's release to accounting; `GET /time-bookings` filters on it with `readyForBilling`; `GET /me` says whether the organization uses it (`organization.readyForBillingEnabled`); and the billing write can require it with `requireReadyForBilling`, refused as `409` `billing_precondition_failed` with `not_ready_for_billing`.\n- **`billingRevision` covers the mark:** setting or lifting it changes the token, and every token issued before this release is stale once.\n- **Customers and projects are writable** (`customers:write`, `projects:write`): `POST`, `PATCH /{id}`, the upsert `PUT /customers/external/{externalId}` and `PUT /projects/external/{externalId}` (with `?adoptExisting=true` to link what was typed in by hand), and all-or-nothing batches of up to 100 at `POST /customers/batch` and `POST /projects/batch` with `dryRun`. Merge semantics, and a body that changes nothing writes nothing. `If-Match` on the upserts and batches refuses a write over a changed record (`412` `precondition_failed`), a weak tag (`W/\"…\"`, as compressed responses carry it) counting as the strong one; `PATCH` takes no entity tag (`If-Match: *` only, no `If-None-Match`, `400` otherwise), since the hosting CDN would answer a successful conditional `PATCH` with its own `412`; the creates and batches take `Idempotency-Key`.\n- **`Customer` and `Project` gain `externalId`** (filterable as `?externalId=`), `createdBy` and `updatedBy` (`RecordActor`, null when unknown), so a sync recognises its own writes, and `Project` gains `colorSet`, false when `color` is the derived one; every existing customer and project `ETag` changes once.\n- **Hourly rates are writable** (`rates:write`, which also needs `money:read` and the accounting feature for every write): `POST /rates`, `PATCH /rates/{id}`, `PUT /rates/external/{externalId}` (adopting an unlinked rate by name) and `POST /rates/batch`, with the same merge, `If-Match` and `Idempotency-Key` rules; `RateCatalogEntry` gains `externalId` (filterable as `?externalId=`), `createdBy` and `updatedBy`, and every existing rate `ETag` changes once. Customer and project writes take `rateExternalId` instead of `rateId`, so a sync names rates by its own ids; like `customerExternalId` on a project, `null` clears it.\n- **Project budget and hours:** `Project` gains `plannedHours`, the budget in hours, which every project write takes. `GET /projects/{id}/hours` (`bookings:read`) sums the project's bookings when asked into spent, billable, ready-for-billing and billed hours, with the running bookings it left out counted.\n- **Reading many records at once:** `GET /customers`, `GET /projects`, `GET /rates` and `GET /tasks` also take `?id=`, up to 100 TimeFusion ids per request, so one call resolves many records instead of one `GET /{id}` each. `GET /customers/external/{externalId}`, `GET /projects/external/{externalId}` and `GET /rates/external/{externalId}` read one record by your id, with the `ETag` the upsert's `If-Match` takes; `404` when none is linked, and `DELETE` there now lists `GET, HEAD, PUT` in `Allow`.\n- **New error codes:** `customer_name_taken`, `external_id_taken`, `ambiguous_adoption`, `customer_not_found`, `template_not_found`, `rate_not_found`, `rate_archived`, `precondition_failed` and `batch_rejected` (`BatchValidationError`), and `money_unavailable` on a `rateId` or `rateExternalId` the key may not set, and on every rate write by such a key (`details[].field` absent then), in its own `MoneyUnavailableError` envelope, which a write's `403` publishes beside `Error`.\n- **A method a path has no operation for is `405` `method_not_allowed`**, with `Allow` listing the methods it does answer: `DELETE` anywhere (nothing is deleted through the API; before, an empty `405`), and `PUT /customers/{id}` and the like (before, `404` `not_found`). A path that exists under no method stays `404`.\n- **Named cross-field rules:** a cross-field rule in `details[].code` is named rather than `custom`: `duplicate` for an `externalId` twice in one batch, `mutually_exclusive` for `rateId` with `rateExternalId`, `customerId` with `customerExternalId`, or `If-Match` with `If-None-Match`.\n- **Starter kit:** drops `TF_BILLING_UNCLEAN_ANALYSIS=include`, gains `TF_BILLING_READY_ONLY`, and gains `push.ts` (`npm run push:dry-run`, `push:execute`, `TF_PUSH_ADOPT_EXISTING`), which pushes an optional `rates` list before customers and projects.\n- **Guide:** gains *Syncing master data*. *Quickstart* routes each kind of integration to its sections and lists the rules every integration follows; *Reading data* now precedes *Billing workflow*; *Query parameters* and *Pagination* move to *API conventions*; cross-references are links, and this changelog lists one release per heading.\n\n#### 1.1.0 (released 2026-09-26)\n\nAdditive. **Every link of the rate chain is readable:** `Project`, `Customer` and `User` gain `rate`, the rate stored on that record, null without money like `Task.rate` (`User.rate` rides `users:read`). Projects and customers are new levels between the task and the person, so `Rate.source` can also be `project` or `customer`. **Rates are references:** every published rate carries `id`, the stored `HourlyRate` it came from; a booking's `pricing.rate.id` names the level that won, null when the rate is frozen. **`GET /rates` and `GET /rates/{id}`** (`masterdata:read`) resolve those ids and flag the organization default; without money only `amount` is null. **Incremental master-data sync:** `/projects`, `/tasks`, `/customers` and `/rates` take `sort=updatedAt` with an optional `from`/`to` window and no span cap. `createdAt` stays the default, a cursor does not carry over to the other ordering, and `from`/`to` without `sort=updatedAt` is `400 invalid_parameters`. Editing a rate moves only the rate's own `updatedAt`, so walk `/rates` to catch it.\n\n#### 1.0.1 (released 2026-09-24)\n\nDocumentation only. *Billing against reviewed state* separates the two billing preconditions: `requireFinalized` needs only `billing:write`, while `expectedBillingRevisions` also needs `bookings:read`, `money:read` and the organization's accounting feature. The starter kit bills without visible money, always sends `requireFinalized` when billing finalized bookings, and renames six settings; its commands name any old name left in `.env`.\n\n#### 1.0.0 (released 2026-09-23)\n\nFirst release. Time bookings and the master data around them, people, invoices, the billing write, conditional reads and the `sort=updatedAt` walk. Invoice amount completeness, a linked quickstart, a downloadable starter kit and OpenAPI 3.2 contract, durable billing retries through `Idempotency-Key`, and read filters that take several values by repeating the parameter.","contact":{"name":"TimeFusion Support","email":"support@timefusion.at","url":"https://timefusion.at"},"termsOfService":"https://timefusion.at/agb"},"servers":[{"url":"https://app.timefusion.at","description":"Your TimeFusion organization's origin."}],"externalDocs":{"url":"https://app.timefusion.at/api/integrations/v1/docs","description":"Browsable API Reference"},"security":[{"bearerAuth":[]}],"tags":[{"name":"API-Key & Organization","description":"Introspection. No scope required."},{"name":"Time bookings","description":"The recorded work an invoice is built from: who worked on what and when, what it is worth, and whether it is billed or locked."},{"name":"Projects","description":"The projects a booking's task belongs to. Reading needs `masterdata:read`; creating and updating needs `projects:write`. See the [Syncing master data](#description/syncing-master-data) guide for keeping them in step with your own system."},{"name":"Tasks","description":"The tasks bookings are recorded against, with their path through the project's task tree. Read-only, behind `masterdata:read`."},{"name":"Customers","description":"The customers projects are billed to. Reading needs `masterdata:read`; creating and updating needs `customers:write`. See the [Syncing master data](#description/syncing-master-data) guide for keeping them in step with your own system."},{"name":"Hourly rates","description":"The organization's rate catalog that projects and customers point at. Reading needs `masterdata:read`, the amounts `money:read` as well; creating and updating needs `rates:write`, `money:read` and the accounting feature. See the [Syncing master data](#description/syncing-master-data) guide for keeping your price list in step."},{"name":"People","description":"The organization's members, as named on bookings. Behind their own `users:read` scope rather than `masterdata:read`, because this resource publishes email addresses."},{"name":"Billing & Invoices","description":"Marking bookings as billed, and the invoices that marking produces. See the [Billing workflow](#description/billing-workflow) guide for the complete invoicing process. For a finalized-only workflow, select bookings with `finalized=true`, `billable=true` and `billed=false`, and send `requireFinalized: true` so the write refuses a booking whose period was reopened in between. Marking applies its own lock, which freezes the booking for its employee independently of any period lock."},{"name":"Documentation","description":"This document, the reference page that renders it, and the starter kit. All public: they take no key."}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"The organization's API key, as `Authorization: Bearer sk_tf_int_live_…`. Never `X-Api-Key`."}},"schemas":{"ApiKeyContext":{"type":"object","properties":{"organization":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Stable identifier of the organization this key belongs to."},"name":{"type":"string","description":"Display name of the organization. It can change, so match on `id`."},"currency":{"type":"string","description":"ISO 4217 code that every monetary field in this API is denominated in. Read it rather than hard-coding a currency; today it is always `EUR`.","example":"EUR"},"timeZone":{"type":"string","description":"IANA time zone the organization works in. Build local period boundaries, such as a calendar month, in this zone; a booking's `finalized` is evaluated against its `beginAt` in this zone too. The UTC offset changes with daylight saving time, so derive it for each date instead of storing one offset. Today it is always `Europe/Vienna`.","example":"Europe/Vienna"},"monthlyCloseStartMonth":{"type":["string","null"],"description":"First month the organization's monthly close applies to, as `YYYY-MM`, or null when the organization does not use it at all: no report is expected of anybody and no monthly-close lock is ever enforced. Read this rather than inferring participation from a person's `monthlyCloseExempt` — with this null, that independently stored flag is moot and `finalized` is determined solely by the organization's change lock. This is a month and not a flag because it says when participation began: months before it have no report of their own, but the lock they eventually fall under is not bounded below, so a booking from an earlier month finalizes together with the first confirmed month. Derived from the same column `finalized` and the `?finalized=` filter are evaluated against, so the three cannot disagree.","example":"2026-07"},"readyForBillingEnabled":{"type":"boolean","description":"Whether project admins in this organization can mark bookings ready for billing (**Abrechnungsbereit** in the app): the organization's accounting feature and its ready-for-billing setting are both on. When false, nobody can mark a booking, so `requireReadyForBilling` refuses every booking not marked while it was on. Those earlier marks stay: their `billing.readyForBilling` is still reported and `?readyForBilling=true` still finds them.","example":true}},"required":["id","name","currency","timeZone","monthlyCloseStartMonth","readyForBillingEnabled"],"description":"The organization this API key belongs to."},"key":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Stable identifier of the API key making this request. Bookings this key marked as billed carry the same value in `billing.by.id`, so you can tell your own writes from another key's without matching on the name.","example":"44444444-4444-4444-8444-444444444444"},"name":{"type":"string","description":"Display name an administrator gave this key. It can change, so match on `id`.","example":"Nori Abrechnung"},"prefix":{"type":"string","description":"First 19 characters of the key. Not secret; use it to recognize the key in your logs or in the organization's API key list.","example":"sk_tf_int_live_a1b2"},"scopes":{"type":"array","items":{"type":"string"},"description":"Scopes granted to this key. They are fixed when the key is created, so a key that needs another scope has to be replaced. A request to an endpoint whose scope is missing here is refused with `403` `forbidden_scope`. The scopes v1 defines: `bookings:read`: Read time bookings. `masterdata:read`: Read projects, tasks, customers and hourly rates. `customers:write`: Create and update customers, including archiving and reactivating them. Setting a customer's hourly rate also requires `money:read`. `projects:write`: Create and update projects, including deactivating and reactivating them. Setting a project's hourly rate also requires `money:read`. `rates:write`: Create and update hourly rates, including archiving and reactivating them. Every rate write also requires `money:read`. `users:read`: Read the organization's people, including their email addresses and hourly rates. `billing:read`: Read invoices. `billing:write`: Mark and unmark time bookings as billed. `money:read`: Include hourly rates and monetary amounts in read responses. Requires the organization's accounting feature to be enabled. Read an unknown value as a scope added after your client was written.","example":["bookings:read","billing:read","billing:write"]},"createdAt":{"type":"string","format":"date-time","description":"When this key was created, in UTC.","example":"2026-08-14T07:30:00.000Z"},"expiresAt":{"type":["string","null"],"format":"date-time","description":"When this key stops working, in UTC, or null if it never expires.","example":"2026-08-14T07:30:00.000Z"}},"required":["id","name","prefix","scopes","createdAt","expiresAt"],"description":"Metadata of the API key making this request. The secret itself is never returned."},"money":{"type":"object","properties":{"visible":{"type":"boolean","description":"Whether monetary fields such as rates and amounts are populated. When false, those fields are still present but null. Requires both the `money:read` scope and the organization's accounting feature."},"unavailableReason":{"type":["string","null"],"description":"Why `visible` is false, or null when it is true. `missing_scope`: This key was not granted `money:read`. Scopes are fixed when a key is created, so this one has to be replaced rather than widened. `accounting_disabled`: The organization does not have the accounting feature enabled, which an administrator has to switch on. Only the first failing condition is reported, so after fixing it, call this endpoint again to check for the other."}},"required":["visible","unavailableReason"],"description":"Whether this key sees monetary values, and if not, why."},"rateLimit":{"type":["object","null"],"properties":{"limit":{"type":"integer","exclusiveMinimum":0,"description":"Read requests this key may make per sliding 60-second window. Writes have a separate budget that is not reported here."},"remaining":{"type":"integer","minimum":0,"description":"Read requests left in the current window, with this request already counted."},"resetAt":{"type":"string","format":"date-time","description":"When the current window resets, in UTC. The same instant as the `X-RateLimit-Reset` header, which gives it in Unix epoch seconds.","example":"2026-08-14T07:30:00.000Z"}},"required":["limit","remaining","resetAt"],"description":"This key's read rate limit as of this request. Null when no limit was counted, because rate limiting is not configured or the limiter was unavailable; the `X-RateLimit-*` headers are absent in the same cases."}},"required":["organization","key","money","rateLimit"],"description":"Everything a key can check before its first real request: the organization it belongs to and that organization's currency and time zone, the key's own metadata and scopes, whether monetary values will be populated, and how much of the rate limit is left.","example":{"organization":{"id":"aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa","name":"Muster GmbH","currency":"EUR","timeZone":"Europe/Vienna","monthlyCloseStartMonth":"2026-07","readyForBillingEnabled":true},"key":{"id":"44444444-4444-4444-8444-444444444444","name":"Nori Abrechnung","prefix":"sk_tf_int_live_a1b2","scopes":["bookings:read","billing:read","billing:write","money:read"],"createdAt":"2026-08-01T10:00:00.000Z","expiresAt":null},"money":{"visible":true,"unavailableReason":null},"rateLimit":{"limit":120,"remaining":119,"resetAt":"2026-08-14T07:31:00.000Z"}}},"BatchItemErrorDetail":{"type":"object","properties":{"index":{"type":"integer","minimum":0,"description":"The refused item's position in `items`, from 0."},"externalId":{"type":"string","description":"The refused item's `externalId`, as sent."},"code":{"type":"string","description":"Why this item was refused: the `error.code` the single upsert answers for the same body, with the same meaning and remedy."},"message":{"type":"string","description":"What is wrong with this item, written for a person. May be reworded in any release; branch on `code`."},"ids":{"type":"array","items":{"type":"string","format":"uuid"},"description":"The records the code refers to: the customer holding the name on `customer_name_taken`, each candidate on `ambiguous_adoption`. Absent on every other code."}},"required":["index","externalId","code","message"],"additionalProperties":false,"description":"One item a master-data batch refused, on the `422`. The batch is all-or-nothing, so nothing was written for any item.","example":{"index":2,"externalId":"K-1003","code":"customer_name_taken","message":"Another customer of this organization already has this name (compared case-insensitively); its id is in `details[]`. Link that customer if it is the one you meant, or choose another name.","ids":["88888888-8888-4888-8888-888888888888"]}},"BatchValidationError":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Machine-readable failure reason — one code per reason, and the value a client branches on. The HTTP status is only the class it falls into. This envelope carries `batch_rejected`. Published as a string rather than an enum, so a reason named in a later release reaches you as an ordinary response: treat a code you do not know as its status class.","example":"batch_rejected"},"message":{"type":"string","description":"Human-readable explanation of the failure. May be reworded in any release; branch on `code`."},"details":{"type":"array","items":{"$ref":"#/components/schemas/BatchItemErrorDetail"},"minItems":1,"description":"Every item the batch refused, in `items` order. Always present and never empty."}},"required":["code","message","details"],"description":"The failure, as one machine-readable `code`, a `message` written for a person, and whatever `details[]` this envelope carries."}},"required":["error"],"description":"The error envelope for a well-formed master-data batch naming items that cannot be written.","example":{"error":{"code":"batch_rejected","message":"1 of 3 items cannot be written, so nothing was written.","details":[{"index":2,"externalId":"K-1003","code":"customer_name_taken","message":"Another customer of this organization already has this name (compared case-insensitively); its id is in `details[]`. Link that customer if it is the one you meant, or choose another name.","ids":["88888888-8888-4888-8888-888888888888"]}]}}},"BilledBy":{"oneOf":[{"$ref":"#/components/schemas/BilledByUser"},{"$ref":"#/components/schemas/BilledByApiKey"}],"discriminator":{"propertyName":"type","mapping":{"user":"#/components/schemas/BilledByUser","api_key":"#/components/schemas/BilledByApiKey"}},"description":"Who marked the booking as billed — a person in the app, or an API key.","example":{"type":"api_key","id":"44444444-4444-4444-8444-444444444444","name":"Invoice sync"}},"BilledByApiKey":{"type":"object","properties":{"type":{"type":"string","enum":["api_key"],"description":"The booking was marked as billed through an API key."},"id":{"type":"string","format":"uuid","description":"Stable identifier of the API key."},"name":{"type":"string","description":"Display name of the API key that marked the booking."}},"required":["type","id","name"],"description":"The API key that marked the booking as billed.","example":{"type":"api_key","id":"44444444-4444-4444-8444-444444444444","name":"Invoice sync"}},"BilledByUser":{"type":"object","properties":{"type":{"type":"string","enum":["user"],"description":"The booking was marked as billed by a person in the app."},"id":{"type":"string","format":"uuid","description":"Stable identifier of the person who marked the booking."},"name":{"type":["string","null"],"description":"Person's display name, or null if unset."}},"required":["type","id","name"],"description":"The person who marked the booking as billed.","example":{"type":"user","id":"66666666-6666-4666-8666-666666666666","name":"Alex Example"}},"BillingErrorDetail":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"The booking id rejected by the billing write."},"code":{"type":"string","description":"Why this booking was rejected; the entire request writes nothing. `not_found`: No such booking. `not_billable`: The booking is not marked billable, so it cannot be put on an invoice. `not_completed`: The booking is still running and has no end time yet. `not_analyzed`: The booking has no booking analysis yet, so it cannot be checked for errors. Retry later; the analysis is recalculated on demand. `analysis_error`: The booking's analysis found an error, so it cannot be billed. The booking's `analysis.errorCode` names it; fix the booking first. `already_billed_elsewhere`: The booking is already billed under a different invoice number. Unmark it first — it is never silently re-pointed. `duplicate_id`: This booking id appears more than once in `bookingIds`."},"message":{"type":"string","description":"What is wrong with this booking, written for a person. May be reworded in any release; branch on `code`."}},"required":["id","code"],"additionalProperties":false,"description":"One booking a billing write refused, on the `422`. The write is all-or-nothing, so an entry here means nothing was written for any id in the request.","example":{"id":"55555555-5555-4555-8555-555555555555","code":"not_completed","message":"The booking is still running and has no end time yet."}},"BillingRequest":{"type":"object","properties":{"bookingIds":{"type":"array","items":{"type":"string","format":"uuid"},"minItems":1,"maxItems":500,"uniqueItems":true,"description":"The bookings to mark or unmark, 1–500 per request. Must be free of duplicates: a repeated id is rejected, so `changed + unchanged === bookingIds.length` always holds on a 200 — the invariant you reconcile against."},"billed":{"type":"boolean","description":"`true` marks the bookings as billed, `false` unmarks them. **Marking freezes a booking for the employee who made it** — they can no longer edit it in the app."},"invoiceNumber":{"type":"string","minLength":1,"maxLength":120,"description":"Your own invoice reference. Required when `billed` is true, and must be omitted when it is false. An invoice with this number is created for the organization if it does not exist yet."},"expectedBillingRevisions":{"type":"object","additionalProperties":{"type":"string","minLength":1,"maxLength":256},"description":"The `billingRevision` you reviewed, keyed by booking id. Send exactly one non-empty token for every id in `bookingIds` and no others. Each token is re-checked against the billing transaction snapshot, so a booking whose inputs moved before that snapshot rejects the whole request with `409` `billing_precondition_failed`, naming that id in `details[]` as `stale_revision`, without disclosing current values. Validation and rate capture use the same snapshot; later rate changes do not change the captured rate. Requires `bookings:read` and visible money: `billing:write` grants neither, so a key that bills blind cannot send this."},"requireFinalized":{"type":"boolean","description":"Refuse the write unless every named booking is `finalized` in the billing transaction snapshot. Valid only when `billed` is true; omitted means false. A booking that is not finalized rejects the whole request with `409` `billing_precondition_failed`, naming that id in `details[]` as `not_finalized`. A period may reopen after that snapshot, including before billing commits; reopening does not undo billing."},"requireReadyForBilling":{"type":"boolean","description":"Refuse the write unless every named booking carries a ready-for-billing mark (`billing.readyForBilling` not null) in the billing transaction snapshot. Valid only when `billed` is true; omitted means false. A booking without the mark rejects the whole request with `409` `billing_precondition_failed`, naming that id in `details[]` as `not_ready_for_billing`. Every named booking is checked, including one already billed under the same invoice. For an organization whose process requires the project admin's release before invoicing; whether the organization uses the mark at all is `organization.readyForBillingEnabled` on `GET /me`. Without this option billing does not require the mark, as in the app."},"dryRun":{"type":"boolean","default":false,"description":"Validate the request and report what it would do, without writing anything. Every rejection a real call can produce is decided before any write, so a dry run returns the same `200`/`400`/`409`/`422` and the same `details[]` a real call would — `changed` then counts the bookings that *would* move. Like a real mark, a dry run first recalculates a missing booking analysis for the named bookings, so `analysis` and `updatedAt` (and the list `ETag`) can move on those bookings. Use it to exercise this endpoint safely: it is the only way to test a billing integration without freezing real bookings for the employees who recorded them. No invoice is created, so `invoice` is null for a number that does not exist yet."}},"required":["bookingIds","billed"],"additionalProperties":false,"description":"The body of a billing write: which bookings to mark or unmark, under which invoice reference, and the optional preconditions and rehearsal flag that make sending it safe. Unknown properties are rejected, so a misspelled `dryrun` can never become a real write.","example":{"bookingIds":["55555555-5555-4555-8555-555555555555"],"billed":true,"invoiceNumber":"RE-2026-0815","dryRun":true}},"BillingResult":{"type":"object","properties":{"invoice":{"anyOf":[{"$ref":"#/components/schemas/InvoiceRef"},{"type":"null"}],"description":"The invoice the bookings are linked to. Null when unmarking, or when a dry run names an invoice that does not exist yet."},"changed":{"type":"integer","minimum":0,"description":"How many bookings this request moved, or would move during a dry run."},"unchanged":{"type":"integer","minimum":0,"description":"How many were already in the target state under the same invoice number. Without an `Idempotency-Key`, identical marks report everything here only absent intervening changes — reconcile an uncertain write before replaying it. A replay under a retained key returns the original counts instead, and says so with `Idempotency-Replayed: true`."},"bookingIds":{"type":"array","items":{"type":"string","format":"uuid"},"minItems":1,"maxItems":500,"uniqueItems":true,"description":"Every id now in the target state — `changed` plus `unchanged` — in request order, so you can reconcile without re-reading."},"dryRun":{"type":"boolean","description":"Whether this response describes a simulation. `false` means the bookings were written; `true` means nothing was, and `changed` counts what a real call would have moved. Assert on it before recording an invoicing run as done."}},"required":["invoice","changed","unchanged","bookingIds","dryRun"],"description":"What a billing write did, or what a dry run would have done. `changed + unchanged` always equals the number of ids sent, and `bookingIds` echoes them in request order, so an invoicing run reconciles without re-reading.","example":{"invoice":{"id":"99999999-9999-4999-8999-999999999999","number":"RE-2026-0815"},"changed":1,"unchanged":0,"bookingIds":["55555555-5555-4555-8555-555555555555"],"dryRun":false}},"BillingValidationError":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Machine-readable failure reason — one code per reason, and the value a client branches on. The HTTP status is only the class it falls into. This envelope carries `validation_failed`. Published as a string rather than an enum, so a reason named in a later release reaches you as an ordinary response: treat a code you do not know as its status class.","example":"validation_failed"},"message":{"type":"string","description":"Human-readable explanation of the failure. May be reworded in any release; branch on `code`."},"details":{"type":"array","items":{"$ref":"#/components/schemas/BillingErrorDetail"},"minItems":1,"description":"Every booking the write rejected, one entry per id. Always present and never empty."}},"required":["code","message","details"],"description":"The failure, as one machine-readable `code`, a `message` written for a person, and whatever `details[]` this envelope carries."}},"required":["error"],"description":"The error envelope for a well-formed billing request naming bookings that cannot be marked as billed.","example":{"error":{"code":"validation_failed","message":"1 of 3 bookings cannot be marked as billed.","details":[{"id":"55555555-5555-4555-8555-555555555555","code":"not_completed","message":"The booking is still running and has no end time yet."}]}}},"Customer":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Stable customer identifier, usable as the `customerId` filter."},"externalId":{"type":["string","null"],"description":"Your system's identifier for this customer, as an integration set it through this API; null when the customer is not linked. Unique per organization among customers, case-sensitive, and shown read-only in the app. Find a customer by it with `?externalId=`, or write it with the upsert."},"name":{"type":"string","description":"Customer display name."},"archived":{"type":"boolean","description":"An archived customer is hidden from new pickers in the app; existing project links are untouched and still returned here."},"rate":{"anyOf":[{"$ref":"#/components/schemas/HourlyRate"},{"type":"null"}],"description":"The customer's own hourly rate: one link of the chain that prices a booking (a rate on the booking itself, then the task's, then the project's, then this one, then the person's, then the organization default, first one set wins), not necessarily the rate any given booking bills. It applies to every project of this customer without a rate of its own, archived customer or not. Null when no rate is stored on it, and when the key may not see money (see `money.visible` on `GET /me`). A rate archived in the app stays attached to the records already using it and is still reported here."},"createdBy":{"anyOf":[{"$ref":"#/components/schemas/RecordActorUser"},{"$ref":"#/components/schemas/RecordActorApiKey"},{"type":"null"}],"description":"Who created this customer. Null when unknown: a customer created before this was recorded, or one whose creator has since been deleted."},"updatedBy":{"anyOf":[{"$ref":"#/components/schemas/RecordActorUser"},{"$ref":"#/components/schemas/RecordActorApiKey"},{"type":"null"}],"description":"Who last changed a published field of this customer; for a new customer, its creator. Null when unknown, as for `createdBy`. To recognize a change you made yourself, compare `updatedBy.id` with `key.id` on `GET /me` **and** `updatedAt` with the one your write returned: a key rotation changes the id, and a reorder in the app moves `updatedAt` without moving `updatedBy`."},"updatedAt":{"type":"string","format":"date-time","description":"When this customer row was last written, in UTC; `?sort=updatedAt` orders by it. Upsert on it rather than comparing whole bodies. It does **not** advance when the rate stored on this customer is edited: a rate's `name` and `amount` live on the rate itself, so changing either moves the rate's own `updatedAt` and leaves this customer row untouched; `GET /rates` is where that change shows up. Assigning a different rate, or clearing it, does move it: which rate applies is a column on this row. The response's `ETag` still changes, because it covers the body.","example":"2026-08-14T07:30:00.000Z"}},"required":["id","externalId","name","archived","rate","createdBy","updatedBy","updatedAt"],"description":"The customer a project is run for. Archived customers are returned and flagged rather than hidden, because a project on last year's invoice still has to resolve to a name.","example":{"id":"88888888-8888-4888-8888-888888888888","externalId":"K-1001","name":"Example GmbH","archived":false,"rate":{"id":"aa000003-0000-4000-8000-000000000003","amount":110,"name":"Rahmenvertrag"},"createdBy":{"type":"user","id":"66666666-6666-4666-8666-666666666666","name":"Alex Example"},"updatedBy":{"type":"api_key","id":"44444444-4444-4444-8444-444444444444","name":"Nori Abrechnung"},"updatedAt":"2026-08-14T09:05:00.000Z"}},"CustomerBatchRequest":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"externalId":{"type":"string","pattern":"^[\\x21-\\x7e](?:[\\x20-\\x7e]{0,126}[\\x21-\\x7e])?$","example":"K-1001","description":"Your system's identifier for this record: 1–128 printable ASCII characters, no leading or trailing spaces, case-sensitive. Unique per organization and resource; the upsert addresses records by it."},"ifMatch":{"type":"string","pattern":"^(?:\\*|(?:W\\/)?\"[\\x21\\x23-\\x7e]*\")$","example":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\"","description":"Optional: this item's `If-Match`. The `ETag` of the record as you last read or wrote it, or `*` to update only; the item is refused with `precondition_failed` when the record linked to its `externalId` no longer has exactly that representation for this key, or (with either value) when none is linked. A weak tag counts as the strong one it was; anything malformed is `400`."},"name":{"type":"string","minLength":1,"maxLength":120,"description":"Customer display name, 1–120 characters after trimming. Unique in the organization, compared case-insensitively: a name another customer has is `409` `customer_name_taken`."},"archived":{"type":"boolean","description":"Archive (`true`) or reactivate (`false`) the customer, as the app does: an archived customer moves to the end of the list and is hidden from new pickers; its projects are untouched. Omit to leave it as it is; a new customer is active."},"rateId":{"type":["string","null"],"format":"uuid","description":"The hourly rate stored on this record, by id from `GET /rates`; `null` clears it. Cannot be combined with `rateExternalId`. Requires `money:read` and the organization's accounting feature, for clearing too (`403` `money_unavailable` otherwise). An archived rate cannot be newly assigned (`422` `rate_archived`), but sending the one already stored is not a change and is accepted."},"rateExternalId":{"type":["string","null"],"pattern":"^[\\x21-\\x7e](?:[\\x20-\\x7e]{0,126}[\\x21-\\x7e])?$","example":"K-1001","description":"The hourly rate, by the `externalId` your system linked it under (`PUT /rates/external/{externalId}`); `null` clears it, as `rateId: null` does. Cannot be combined with `rateId`. The same money gate and archived rule as `rateId`. No rate linked to it is `422` `rate_not_found`: send the rate first."}},"required":["externalId","name"],"additionalProperties":false,"description":"One customer, as `PUT /customers/external/{externalId}` takes it, with the path's `externalId` and the `If-Match` header as fields. Unknown properties are rejected."},"minItems":1,"maxItems":100,"description":"1–100 customers, each upserted by its `externalId` exactly as `PUT /customers/external/{externalId}` upserts one, in this order. Each `externalId` at most once per batch."},"dryRun":{"type":"boolean","default":false,"description":"Run the whole batch and report what it would do, then write nothing. Every refusal a real call meets is met, so a dry run answers the same `200`, `403` or `422` a real call would. A dry run takes no `Idempotency-Key`."},"adoptExisting":{"type":"boolean","default":false,"description":"`adoptExisting` of the single upsert, for every item: when no record is linked to an item's `externalId` yet, link an existing unlinked one instead of creating a new one. Off by default."}},"required":["items"],"additionalProperties":false,"description":"Customers to upsert by `externalId` in one all-or-nothing transaction. Unknown properties are rejected.","example":{"items":[{"externalId":"K-1001","name":"Example GmbH"},{"externalId":"K-1002","name":"Muster AG","archived":true}],"adoptExisting":true}},"CustomerBatchResult":{"type":"object","properties":{"dryRun":{"type":"boolean","description":"Whether this response describes a simulation. `false` means the items were written; `true` means nothing was."},"created":{"type":"integer","minimum":0,"description":"How many customers were created."},"updated":{"type":"integer","minimum":0,"description":"How many existing customers were changed, adoptions included (linking writes the `externalId`)."},"unchanged":{"type":"integer","minimum":0,"description":"How many customers already matched their item, so nothing was written for them."},"items":{"type":"array","items":{"type":"object","properties":{"externalId":{"type":"string","description":"The item's `externalId`, as sent."},"outcome":{"type":"string","enum":["created","updated","unchanged"],"description":"`created`: no record was linked and none adopted, so one was created. `updated`: a linked or adopted record was changed. `unchanged`: the linked record already matched, so nothing was written and its `updatedAt` and `ETag` stayed put."},"id":{"type":["string","null"],"format":"uuid","description":"The record's id. `null` only on a dry run's `created` item, whose record does not exist."},"record":{"allOf":[{"$ref":"#/components/schemas/Customer"},{"type":["object","null"],"description":"The record as `GET` by id returns it for this key, as it now is. `null` on a dry run."}]},"etag":{"type":["string","null"],"description":"The record's `ETag`, as the single write's `ETag` header would carry it: store it to send as this item's `ifMatch` next time. `null` on a dry run."}},"required":["externalId","outcome","id","record","etag"]},"description":"One entry per item, in request order."}},"required":["dryRun","created","updated","unchanged","items"],"description":"What a customer batch did, or what a dry run would have done. `created + updated + unchanged` always equals the number of items, and `items` answers them in request order.","example":{"dryRun":false,"created":0,"updated":1,"unchanged":0,"items":[{"externalId":"K-1001","outcome":"updated","id":"88888888-8888-4888-8888-888888888888","record":{"id":"88888888-8888-4888-8888-888888888888","externalId":"K-1001","name":"Example GmbH","archived":false,"rate":{"id":"aa000003-0000-4000-8000-000000000003","amount":110,"name":"Rahmenvertrag"},"createdBy":{"type":"user","id":"66666666-6666-4666-8666-666666666666","name":"Alex Example"},"updatedBy":{"type":"api_key","id":"44444444-4444-4444-8444-444444444444","name":"Nori Abrechnung"},"updatedAt":"2026-08-14T09:05:00.000Z"},"etag":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\""}]}},"CustomerCreateRequest":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120,"description":"Customer display name, 1–120 characters after trimming. Unique in the organization, compared case-insensitively: a name another customer has is `409` `customer_name_taken`."},"archived":{"type":"boolean","description":"Archive (`true`) or reactivate (`false`) the customer, as the app does: an archived customer moves to the end of the list and is hidden from new pickers; its projects are untouched. Omit to leave it as it is; a new customer is active."},"rateId":{"type":["string","null"],"format":"uuid","description":"The hourly rate stored on this record, by id from `GET /rates`; `null` clears it. Cannot be combined with `rateExternalId`. Requires `money:read` and the organization's accounting feature, for clearing too (`403` `money_unavailable` otherwise). An archived rate cannot be newly assigned (`422` `rate_archived`), but sending the one already stored is not a change and is accepted."},"rateExternalId":{"type":["string","null"],"pattern":"^[\\x21-\\x7e](?:[\\x20-\\x7e]{0,126}[\\x21-\\x7e])?$","example":"K-1001","description":"The hourly rate, by the `externalId` your system linked it under (`PUT /rates/external/{externalId}`); `null` clears it, as `rateId: null` does. Cannot be combined with `rateId`. The same money gate and archived rule as `rateId`. No rate linked to it is `422` `rate_not_found`: send the rate first."}},"required":["name"],"additionalProperties":false,"description":"A new customer. Unknown properties are rejected. To link it to your own id, create it with `PUT /customers/external/{externalId}` instead.","example":{"name":"Example GmbH"}},"CustomerPage":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Customer"},"description":"The records on this page, in cursor order."},"pagination":{"$ref":"#/components/schemas/Pagination"}},"required":["data","pagination"],"description":"One page of customers, oldest first. Pass `pagination.nextCursor` back as `cursor` and stop when `hasMore` is false; there is no total count.","example":{"data":[{"id":"88888888-8888-4888-8888-888888888888","externalId":"K-1001","name":"Example GmbH","archived":false,"rate":{"id":"aa000003-0000-4000-8000-000000000003","amount":110,"name":"Rahmenvertrag"},"createdBy":{"type":"user","id":"66666666-6666-4666-8666-666666666666","name":"Alex Example"},"updatedBy":{"type":"api_key","id":"44444444-4444-4444-8444-444444444444","name":"Nori Abrechnung"},"updatedAt":"2026-08-14T09:05:00.000Z"}],"pagination":{"nextCursor":null,"hasMore":false}}},"CustomerPatchRequest":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120,"description":"Customer display name, 1–120 characters after trimming. Unique in the organization, compared case-insensitively: a name another customer has is `409` `customer_name_taken`."},"archived":{"type":"boolean","description":"Archive (`true`) or reactivate (`false`) the customer, as the app does: an archived customer moves to the end of the list and is hidden from new pickers; its projects are untouched. Omit to leave it as it is; a new customer is active."},"rateId":{"type":["string","null"],"format":"uuid","description":"The hourly rate stored on this record, by id from `GET /rates`; `null` clears it. Cannot be combined with `rateExternalId`. Requires `money:read` and the organization's accounting feature, for clearing too (`403` `money_unavailable` otherwise). An archived rate cannot be newly assigned (`422` `rate_archived`), but sending the one already stored is not a change and is accepted."},"rateExternalId":{"type":["string","null"],"pattern":"^[\\x21-\\x7e](?:[\\x20-\\x7e]{0,126}[\\x21-\\x7e])?$","example":"K-1001","description":"The hourly rate, by the `externalId` your system linked it under (`PUT /rates/external/{externalId}`); `null` clears it, as `rateId: null` does. Cannot be combined with `rateId`. The same money gate and archived rule as `rateId`. No rate linked to it is `422` `rate_not_found`: send the rate first."},"externalId":{"type":["string","null"],"pattern":"^[\\x21-\\x7e](?:[\\x20-\\x7e]{0,126}[\\x21-\\x7e])?$","example":"K-1001","description":"Your system's identifier for this record: 1–128 printable ASCII characters, no leading or trailing spaces, case-sensitive. Unique per organization and resource; the upsert addresses records by it. `null` unlinks the record, which is how a client re-links: clear it here, then set it on the other record. Omit to leave it as it is."}},"additionalProperties":false,"description":"The fields to change. Merge semantics: an omitted field is left as stored, `null` clears a nullable one. A body that changes nothing writes nothing, so `updatedAt` and the `ETag` stay as they were. Unknown properties are rejected.","example":{"archived":true}},"CustomerRef":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Stable customer identifier, usable as the `customerId` filter."},"name":{"type":"string","description":"Customer display name."}},"required":["id","name"],"description":"A customer, as referenced from another resource.","example":{"id":"88888888-8888-4888-8888-888888888888","name":"Example GmbH"}},"CustomerUpsertRequest":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120,"description":"Customer display name, 1–120 characters after trimming. Unique in the organization, compared case-insensitively: a name another customer has is `409` `customer_name_taken`."},"archived":{"type":"boolean","description":"Archive (`true`) or reactivate (`false`) the customer, as the app does: an archived customer moves to the end of the list and is hidden from new pickers; its projects are untouched. Omit to leave it as it is; a new customer is active."},"rateId":{"type":["string","null"],"format":"uuid","description":"The hourly rate stored on this record, by id from `GET /rates`; `null` clears it. Cannot be combined with `rateExternalId`. Requires `money:read` and the organization's accounting feature, for clearing too (`403` `money_unavailable` otherwise). An archived rate cannot be newly assigned (`422` `rate_archived`), but sending the one already stored is not a change and is accepted."},"rateExternalId":{"type":["string","null"],"pattern":"^[\\x21-\\x7e](?:[\\x20-\\x7e]{0,126}[\\x21-\\x7e])?$","example":"K-1001","description":"The hourly rate, by the `externalId` your system linked it under (`PUT /rates/external/{externalId}`); `null` clears it, as `rateId: null` does. Cannot be combined with `rateId`. The same money gate and archived rule as `rateId`. No rate linked to it is `422` `rate_not_found`: send the rate first."}},"required":["name"],"additionalProperties":false,"description":"The customer linked to the path's `externalId`, as it should be. Merge semantics: an omitted field is left as stored on an update, `null` clears a nullable one. `name` is required either way, because the upsert may create. Unknown properties are rejected.","example":{"name":"Example GmbH"}},"Error":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Machine-readable failure reason — one code per reason, and the value a client branches on. The HTTP status is only the class it falls into. This envelope carries `unauthorized`, `forbidden_scope`, `not_found`, `method_not_allowed`, `billing_precondition_failed`, `conflict`, `invoice_page_changed`, `idempotency_key_reused`, `idempotency_request_in_progress`, `customer_name_taken`, `external_id_taken`, `ambiguous_adoption`, `precondition_failed`, `customer_not_found`, `template_not_found`, `rate_not_found`, `rate_archived`, `rate_limited`, `internal_error`. Published as a string rather than an enum, so a reason named in a later release reaches you as an ordinary response: treat a code you do not know as its status class.","example":"unauthorized"},"message":{"type":"string","description":"Human-readable explanation of the failure. May be reworded in any release; branch on `code`."},"details":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"The booking that failed the precondition, the customer holding the name, or the candidate project, customer or rate."},"code":{"type":"string","description":"What this entry is. `stale_revision`: the token sent for the booking in `expectedBillingRevisions` does not match its current billing inputs, because they changed since the read or the token is not one this API issued for this booking. `not_finalized`: `requireFinalized` was set and the booking is not `finalized`. `not_ready_for_billing`: `requireReadyForBilling` was set and the booking's `billing.readyForBilling` is null. `existing_customer`: on `customer_name_taken`, the customer that already has the name. `adoption_candidate`: on `ambiguous_adoption`, one of the unlinked projects carrying the `projectNumber`, or of the unlinked customers or rates with the name, compared case-insensitively."}},"required":["id","code"]},"description":"Per-booking failures on `billing_precondition_failed`, one entry per failed precondition, so a booking failing both is listed twice; no current values are disclosed. The customer holding the name on `customer_name_taken`, and each candidate on `ambiguous_adoption`."},"requestId":{"type":"string","description":"Correlation ID, present on 500 responses."}},"required":["code","message"],"description":"The failure, as one machine-readable `code`, a `message` written for a person, and whatever `details[]` this envelope carries."}},"required":["error"],"description":"The error envelope for a single request-level failure. `code` names the reason. Three codes carry `details[]` here: `billing_precondition_failed` (one entry per failed booking precondition), `customer_name_taken` (the customer holding the name) and `ambiguous_adoption` (one entry per candidate project, customer or rate); every other code in this class carries none.","example":{"error":{"code":"unauthorized","message":"Missing or invalid API key. Send it as `Authorization: Bearer sk_tf_int_live_…`."}}},"FieldErrorDetail":{"type":"object","properties":{"field":{"type":"string","description":"Dot-separated request field path; omitted for a root-level validation failure."},"code":{"type":"string","description":"Field-validation issue code, such as `invalid_type`, `too_big` or `custom`; not a billing rejection code. A rule spanning fields has its own: `duplicate` for a value repeated where it must be unique (an `externalId` within one batch), `mutually_exclusive` for two fields that name the same thing sent together (`rateId` and `rateExternalId`, `customerId` and `customerExternalId`, `If-Match` and `If-None-Match`; `field` is the second). This is the machine surface — branch on it, not on `message`."},"message":{"type":"string","description":"What is wrong with this request field, written for a person. May be reworded in any release; branch on `code`."}},"required":["code"],"additionalProperties":false,"description":"One request field a `400` rejected. It names the field path and a machine-readable issue code, so a client can point at the offending input rather than parse the sentence.","example":{"field":"limit","code":"too_big","message":"`limit` must be at most 200."}},"HourlyRate":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Stable identifier of the stored `HourlyRate` row. The reference to keep: it survives a rename, so an integrator can store it and join it against the rate catalog (`GET /rates`) instead of matching on `name` and `amount`. Two rates sharing a label and an amount stay distinguishable by it."},"amount":{"type":"number","description":"The stored rate per hour, in the organization's currency; see `organization.currency` on `GET /me`. Not necessarily what a booking bills: a level earlier in the chain that prices a booking (see `Rate.source`) outranks it, and an accounted booking bills the amount frozen at billing time."},"name":{"type":"string","description":"Label of the rate, e.g. \"Standard\". Never null here, unlike `Rate.name` on a booking, which can be a frozen snapshot that recorded none."}},"required":["id","amount","name"],"description":"An hourly rate stored on a record — a task, project, customer or person. Not the rate any given booking bills — that is `Rate`, which also records how the booking resolved its price.","example":{"id":"aa000001-0000-4000-8000-000000000001","amount":120,"name":"Standard"}},"Invoice":{"type":"object","properties":{"invoice":{"$ref":"#/components/schemas/InvoiceRef"},"createdAt":{"type":"string","format":"date-time","description":"When the invoice reference was first used, in UTC. Not the date on your own invoice.","example":"2026-08-14T07:30:00.000Z"},"timeBookingCount":{"type":"integer","minimum":0,"description":"How many **time bookings** are linked. Project expenses are counted separately, as `expenseCount`. An invoice whose bookings were all unmarked reports 0 and stays listed — the row still exists."},"timeBookingAmount":{"type":["number","null"],"description":"Sum of the linked time bookings' amounts in the organization's currency, each rounded to 2 decimals first, so it matches what you get by summing a `GET /time-bookings` page. Null when the key may not see money, and null — never `0` — when the invoice has linked bookings but none of them could be priced. An invoice with no linked bookings reports `0`. Excludes unpriced bookings, and excludes the project expenses on the same invoice — those are `expenseCount` and `expenseAmount`."},"unpricedTimeBookingCount":{"type":["integer","null"],"minimum":0,"description":"How many linked time bookings are left out of `timeBookingAmount` because they have no amount: not billable, no rate, or no duration. A booking priced at exactly `0` is priced and not counted here. Null when the key may not see money."},"timeBookingAmountComplete":{"type":["boolean","null"],"description":"True when every linked booking has a known amount (including an empty invoice), false for partial or entirely unpriced totals, null when money is hidden."},"expenseCount":{"type":"integer","minimum":0,"description":"How many **project expenses** are linked to this invoice. Expenses are recorded in the app and are not otherwise exposed in v1; use this count to explain a total that differs from `timeBookingAmount`. Populated even when the key may not see money, like `timeBookingCount`."},"expenseAmount":{"type":["number","null"],"description":"Sum of the linked project expenses in the organization's currency, rounded to 2 decimals, or null when the key may not see money. An invoice with no expenses reports `0` — unlike `timeBookingAmount`, there is no unpriced case here: an expense carries its own stored amount. Do **not** add it to `timeBookingAmount` and treat the result as the customer's invoice total; that document can carry tax, discounts and lines TimeFusion never saw."}},"required":["invoice","createdAt","timeBookingCount","timeBookingAmount","unpricedTimeBookingCount","timeBookingAmountComplete","expenseCount","expenseAmount"],"description":"An invoice reference bookings were billed under, and what TimeFusion knows about it: when the number was first used, and how many time bookings and project expenses are linked. The two kinds are counted and totaled separately and are never added together here, because your own invoice can carry tax, discounts and lines TimeFusion never saw. To read the bookings themselves, use `GET /time-bookings?invoiceNumber=`.","example":{"invoice":{"id":"99999999-9999-4999-8999-999999999999","number":"RE-2026-0815"},"createdAt":"2026-08-31T12:00:00.000Z","timeBookingCount":1,"timeBookingAmount":180,"unpricedTimeBookingCount":0,"timeBookingAmountComplete":true,"expenseCount":2,"expenseAmount":74.5}},"InvoicePage":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Invoice"},"description":"The records on this page, in cursor order."},"pagination":{"$ref":"#/components/schemas/Pagination"}},"required":["data","pagination"],"description":"One page of invoices, oldest first. Pass `pagination.nextCursor` back as `cursor` and stop when `hasMore` is false; there is no total count.","example":{"data":[{"invoice":{"id":"99999999-9999-4999-8999-999999999999","number":"RE-2026-0815"},"createdAt":"2026-08-31T12:00:00.000Z","timeBookingCount":1,"timeBookingAmount":180,"unpricedTimeBookingCount":0,"timeBookingAmountComplete":true,"expenseCount":2,"expenseAmount":74.5}],"pagination":{"nextCursor":null,"hasMore":false}}},"InvoiceRef":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Stable identifier of the invoice record in TimeFusion."},"number":{"type":"string","description":"Invoice reference number, as entered in the app or sent as `invoiceNumber` in a billing write. Unique within the organization."}},"required":["id","number"],"description":"An invoice, as referenced from another resource.","example":{"id":"99999999-9999-4999-8999-999999999999","number":"RE-2026-0815"}},"MoneyUnavailableDetail":{"type":"object","properties":{"field":{"type":"string","description":"The request field the key may not set: `rateId` or `rateExternalId`, or `items.N.rateId` / `items.N.rateExternalId` on a batch, naming the first item that sends one. Absent on a rate write, which needs money whatever its body sends."},"code":{"type":"string","description":"Why the key may not set it, or write rates at all, in `GET /me`'s `money.unavailableReason` vocabulary."}},"required":["code"],"additionalProperties":false,"description":"Why a master-data write needs money the key may not see, on `money_unavailable`.","example":{"field":"rateExternalId","code":"missing_scope"}},"MoneyUnavailableError":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Machine-readable failure reason — one code per reason, and the value a client branches on. The HTTP status is only the class it falls into. This envelope carries `money_unavailable`. Published as a string rather than an enum, so a reason named in a later release reaches you as an ordinary response: treat a code you do not know as its status class.","example":"money_unavailable"},"message":{"type":"string","description":"Human-readable explanation of the failure. May be reworded in any release; branch on `code`."},"details":{"type":"array","items":{"$ref":"#/components/schemas/MoneyUnavailableDetail"},"minItems":1,"description":"Why, and the field when one is the cause. Always present, one entry."}},"required":["code","message","details"],"description":"The failure, as one machine-readable `code`, a `message` written for a person, and whatever `details[]` this envelope carries."}},"required":["error"],"description":"The error envelope for a master-data write that needs money the API key may not see.","example":{"error":{"code":"money_unavailable","message":"Setting `rateExternalId` requires the `money:read` scope. Scopes are fixed at key creation — issue a new key to change them.","details":[{"field":"rateExternalId","code":"missing_scope"}]}}},"Pagination":{"type":"object","properties":{"nextCursor":{"type":["string","null"],"description":"Opaque cursor for the next page. Pass it back as `cursor`. Null when `hasMore` is false."},"hasMore":{"type":"boolean","description":"Whether another page exists after this one."}},"required":["nextCursor","hasMore"],"description":"Pagination state for this page; no total count is calculated.","example":{"nextCursor":null,"hasMore":false}},"Project":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Stable project identifier, usable as the `projectId` filter."},"externalId":{"type":["string","null"],"description":"Your system's identifier for this project, as an integration set it through this API; null when the project is not linked. Unique per organization among projects, case-sensitive, and shown read-only in the app. Find a project by it with `?externalId=`, or write it with the upsert."},"name":{"type":"string","description":"Project display name."},"projectNumber":{"type":["string","null"],"description":"Organization-defined project reference, or null if unset."},"color":{"type":"string","description":"Effective board color as `#RRGGBB`. When no color was set, this is the stable color derived from the project identifier."},"colorSet":{"type":"boolean","description":"Whether `color` was set on the project. False when `color` is the derived one, which a write may send back without pinning it."},"deactivated":{"type":"boolean","description":"Whether the project is deactivated. A deactivated project takes no new bookings; its historical bookings are still returned and still billable."},"customer":{"anyOf":[{"$ref":"#/components/schemas/CustomerRef"},{"type":"null"}],"description":"Customer linked to this project, or null if unassigned."},"rate":{"anyOf":[{"$ref":"#/components/schemas/HourlyRate"},{"type":"null"}],"description":"The project's own hourly rate: one link of the chain that prices a booking (a rate on the booking itself, then the task's, then this one, then the customer's, then the person's, then the organization default, first one set wins), not necessarily the rate any given booking bills. It prices every booking of the project that has no rate of its own and whose task has none. Null when no rate is stored on it, and when the key may not see money (see `money.visible` on `GET /me`). A rate archived in the app stays attached to the records already using it and is still reported here."},"plannedHours":{"type":["number","null"],"description":"The project's budget in hours, as set on the project in the app, or null if none is set. The hours spent against it are on `GET /projects/{id}/hours`, which needs `bookings:read`."},"createdBy":{"anyOf":[{"$ref":"#/components/schemas/RecordActorUser"},{"$ref":"#/components/schemas/RecordActorApiKey"},{"type":"null"}],"description":"Who created this project. Null when unknown: a project created before this was recorded, or one whose creator has since been deleted."},"updatedBy":{"anyOf":[{"$ref":"#/components/schemas/RecordActorUser"},{"$ref":"#/components/schemas/RecordActorApiKey"},{"type":"null"}],"description":"Who last changed a published field of this project; for a new project, its creator. Null when unknown, as for `createdBy`. To recognize a change you made yourself, compare `updatedBy.id` with `key.id` on `GET /me` **and** `updatedAt` with the one your write returned: a key rotation changes the id, and a reorder in the app moves `updatedAt` without moving `updatedBy`."},"updatedAt":{"type":"string","format":"date-time","description":"When this project row was last written, in UTC; `?sort=updatedAt` orders by it. Upsert on it rather than comparing whole bodies. It does **not** advance when the `customer` reached through it is renamed on its own row — that name lives on the customer, and `GET /customers` is where a change to it shows up. It also does **not** advance when this project's `rate` is edited: a rate's `name` and `amount` live on the rate itself, so changing either moves the rate's own `updatedAt` and leaves this project row untouched; `GET /rates` is where that change shows up. Assigning a different rate, or clearing it, does move it: which rate applies is a column on this row. The response's `ETag` still changes, because it covers the body.","example":"2026-08-14T07:30:00.000Z"}},"required":["id","externalId","name","projectNumber","color","colorSet","deactivated","customer","rate","plannedHours","createdBy","updatedBy","updatedAt"],"description":"A project, as the work a booking is recorded against: the record `TimeBooking.project` refers to, carrying the customer behind it and whether it still takes new bookings. Its hours budget is `plannedHours`; the hours spent against it are not part of the record, so recording time does not change the project's `updatedAt` or `ETag`, and are on `GET /projects/{id}/hours`.","example":{"id":"77777777-7777-4777-8777-777777777777","externalId":"P-2026-001","name":"Customer portal","projectNumber":"P-2026-001","color":"#228BE6","colorSet":true,"deactivated":false,"customer":{"id":"88888888-8888-4888-8888-888888888888","name":"Example GmbH"},"rate":{"id":"aa000002-0000-4000-8000-000000000002","amount":135,"name":"Portal-Pauschale"},"plannedHours":120,"createdBy":{"type":"api_key","id":"44444444-4444-4444-8444-444444444444","name":"Nori Abrechnung"},"updatedBy":{"type":"api_key","id":"44444444-4444-4444-8444-444444444444","name":"Nori Abrechnung"},"updatedAt":"2026-08-14T09:05:00.000Z"}},"ProjectBatchRequest":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"externalId":{"type":"string","pattern":"^[\\x21-\\x7e](?:[\\x20-\\x7e]{0,126}[\\x21-\\x7e])?$","example":"K-1001","description":"Your system's identifier for this record: 1–128 printable ASCII characters, no leading or trailing spaces, case-sensitive. Unique per organization and resource; the upsert addresses records by it."},"ifMatch":{"type":"string","pattern":"^(?:\\*|(?:W\\/)?\"[\\x21\\x23-\\x7e]*\")$","example":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\"","description":"Optional: this item's `If-Match`. The `ETag` of the record as you last read or wrote it, or `*` to update only; the item is refused with `precondition_failed` when the record linked to its `externalId` no longer has exactly that representation for this key, or (with either value) when none is linked. A weak tag counts as the strong one it was; anything malformed is `400`."},"name":{"type":"string","minLength":3,"maxLength":255,"description":"Project display name, 3–255 characters after trimming."},"projectNumber":{"type":["string","null"],"minLength":1,"maxLength":64,"description":"Your project reference, 1–64 characters after trimming; `null` clears it. Not unique: the app does not require it to be."},"color":{"type":["string","null"],"pattern":"^#[0-9a-fA-F]{6}$","description":"Board color as `#RRGGBB` (stored lower-case); `null` returns the project to the stable color derived from its id, which is what `color` on the read reports then, with `colorSet` false. Sending that derived color back unchanged is not a change and does not pin it."},"customerId":{"type":["string","null"],"format":"uuid","description":"The customer, by TimeFusion id; `null` removes it. Cannot be combined with `customerExternalId`. An id this organization does not have is `422` `customer_not_found`."},"customerExternalId":{"type":["string","null"],"pattern":"^[\\x21-\\x7e](?:[\\x20-\\x7e]{0,126}[\\x21-\\x7e])?$","example":"K-1001","description":"The customer, by the `externalId` your system linked it under; `null` removes it, as `customerId: null` does. Cannot be combined with `customerId`. No customer linked to it is `422` `customer_not_found`: send the customer first. There is no find-or-create by name here, since that would need `customers:write`."},"rateId":{"type":["string","null"],"format":"uuid","description":"The hourly rate stored on this record, by id from `GET /rates`; `null` clears it. Cannot be combined with `rateExternalId`. Requires `money:read` and the organization's accounting feature, for clearing too (`403` `money_unavailable` otherwise). An archived rate cannot be newly assigned (`422` `rate_archived`), but sending the one already stored is not a change and is accepted."},"rateExternalId":{"type":["string","null"],"pattern":"^[\\x21-\\x7e](?:[\\x20-\\x7e]{0,126}[\\x21-\\x7e])?$","example":"K-1001","description":"The hourly rate, by the `externalId` your system linked it under (`PUT /rates/external/{externalId}`); `null` clears it, as `rateId: null` does. Cannot be combined with `rateId`. The same money gate and archived rule as `rateId`. No rate linked to it is `422` `rate_not_found`: send the rate first."},"plannedHours":{"type":["number","null"],"minimum":0,"maximum":1000000,"description":"The project's budget in hours, 0–1000000 with at most two decimals; `null` removes it. The app compares the hours spent against it; `GET /projects/{id}/hours` reports the same."},"deactivated":{"type":"boolean","description":"Deactivate (`true`) or reactivate (`false`) the project, as the app does: a deactivated project takes no new bookings, moves to the end of the list, and stops being anyone's default task. Its bookings stay. Omit to leave it as it is; a new project is active."},"templateProjectId":{"type":"string","format":"uuid","description":"Create from this project as a template, as the app does: its members with their project roles and its task tree are copied, and its customer and (unless archived) its rate apply when the body names none. Without it the project has no members and no tasks, since a key is not a person who could be its first member; someone with project management rights finishes the setup in the app. A project this organization does not have is `422` `template_not_found`. Applied only when this item creates the project."}},"required":["externalId","name"],"additionalProperties":false,"description":"One project, as `PUT /projects/external/{externalId}` takes it, with the path's `externalId` and the `If-Match` header as fields. Unknown properties are rejected."},"minItems":1,"maxItems":100,"description":"1–100 projects, each upserted by its `externalId` exactly as `PUT /projects/external/{externalId}` upserts one, in this order. Each `externalId` at most once per batch."},"dryRun":{"type":"boolean","default":false,"description":"Run the whole batch and report what it would do, then write nothing. Every refusal a real call meets is met, so a dry run answers the same `200`, `403` or `422` a real call would. A dry run takes no `Idempotency-Key`."},"adoptExisting":{"type":"boolean","default":false,"description":"`adoptExisting` of the single upsert, for every item: when no record is linked to an item's `externalId` yet, link an existing unlinked one instead of creating a new one. Off by default."}},"required":["items"],"additionalProperties":false,"description":"Projects to upsert by `externalId` in one all-or-nothing transaction. A project may name a customer by `customerExternalId`, so send a sync's customer batch first. Unknown properties are rejected.","example":{"items":[{"externalId":"P-2026-001","name":"Customer portal","projectNumber":"P-2026-001","customerExternalId":"K-1001"},{"externalId":"P-2025-014","name":"Website relaunch","deactivated":true}],"adoptExisting":true}},"ProjectBatchResult":{"type":"object","properties":{"dryRun":{"type":"boolean","description":"Whether this response describes a simulation. `false` means the items were written; `true` means nothing was."},"created":{"type":"integer","minimum":0,"description":"How many projects were created."},"updated":{"type":"integer","minimum":0,"description":"How many existing projects were changed, adoptions included (linking writes the `externalId`)."},"unchanged":{"type":"integer","minimum":0,"description":"How many projects already matched their item, so nothing was written for them."},"items":{"type":"array","items":{"type":"object","properties":{"externalId":{"type":"string","description":"The item's `externalId`, as sent."},"outcome":{"type":"string","enum":["created","updated","unchanged"],"description":"`created`: no record was linked and none adopted, so one was created. `updated`: a linked or adopted record was changed. `unchanged`: the linked record already matched, so nothing was written and its `updatedAt` and `ETag` stayed put."},"id":{"type":["string","null"],"format":"uuid","description":"The record's id. `null` only on a dry run's `created` item, whose record does not exist."},"record":{"allOf":[{"$ref":"#/components/schemas/Project"},{"type":["object","null"],"description":"The record as `GET` by id returns it for this key, as it now is. `null` on a dry run."}]},"etag":{"type":["string","null"],"description":"The record's `ETag`, as the single write's `ETag` header would carry it: store it to send as this item's `ifMatch` next time. `null` on a dry run."}},"required":["externalId","outcome","id","record","etag"]},"description":"One entry per item, in request order."}},"required":["dryRun","created","updated","unchanged","items"],"description":"What a project batch did, or what a dry run would have done. `created + updated + unchanged` always equals the number of items, and `items` answers them in request order.","example":{"dryRun":false,"created":1,"updated":0,"unchanged":0,"items":[{"externalId":"P-2026-001","outcome":"created","id":"77777777-7777-4777-8777-777777777777","record":{"id":"77777777-7777-4777-8777-777777777777","externalId":"P-2026-001","name":"Customer portal","projectNumber":"P-2026-001","color":"#228BE6","colorSet":true,"deactivated":false,"customer":{"id":"88888888-8888-4888-8888-888888888888","name":"Example GmbH"},"rate":{"id":"aa000002-0000-4000-8000-000000000002","amount":135,"name":"Portal-Pauschale"},"plannedHours":120,"createdBy":{"type":"api_key","id":"44444444-4444-4444-8444-444444444444","name":"Nori Abrechnung"},"updatedBy":{"type":"api_key","id":"44444444-4444-4444-8444-444444444444","name":"Nori Abrechnung"},"updatedAt":"2026-08-14T09:05:00.000Z"},"etag":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\""}]}},"ProjectCreateRequest":{"type":"object","properties":{"name":{"type":"string","minLength":3,"maxLength":255,"description":"Project display name, 3–255 characters after trimming."},"projectNumber":{"type":["string","null"],"minLength":1,"maxLength":64,"description":"Your project reference, 1–64 characters after trimming; `null` clears it. Not unique: the app does not require it to be."},"color":{"type":["string","null"],"pattern":"^#[0-9a-fA-F]{6}$","description":"Board color as `#RRGGBB` (stored lower-case); `null` returns the project to the stable color derived from its id, which is what `color` on the read reports then, with `colorSet` false. Sending that derived color back unchanged is not a change and does not pin it."},"customerId":{"type":["string","null"],"format":"uuid","description":"The customer, by TimeFusion id; `null` removes it. Cannot be combined with `customerExternalId`. An id this organization does not have is `422` `customer_not_found`."},"customerExternalId":{"type":["string","null"],"pattern":"^[\\x21-\\x7e](?:[\\x20-\\x7e]{0,126}[\\x21-\\x7e])?$","example":"K-1001","description":"The customer, by the `externalId` your system linked it under; `null` removes it, as `customerId: null` does. Cannot be combined with `customerId`. No customer linked to it is `422` `customer_not_found`: send the customer first. There is no find-or-create by name here, since that would need `customers:write`."},"rateId":{"type":["string","null"],"format":"uuid","description":"The hourly rate stored on this record, by id from `GET /rates`; `null` clears it. Cannot be combined with `rateExternalId`. Requires `money:read` and the organization's accounting feature, for clearing too (`403` `money_unavailable` otherwise). An archived rate cannot be newly assigned (`422` `rate_archived`), but sending the one already stored is not a change and is accepted."},"rateExternalId":{"type":["string","null"],"pattern":"^[\\x21-\\x7e](?:[\\x20-\\x7e]{0,126}[\\x21-\\x7e])?$","example":"K-1001","description":"The hourly rate, by the `externalId` your system linked it under (`PUT /rates/external/{externalId}`); `null` clears it, as `rateId: null` does. Cannot be combined with `rateId`. The same money gate and archived rule as `rateId`. No rate linked to it is `422` `rate_not_found`: send the rate first."},"plannedHours":{"type":["number","null"],"minimum":0,"maximum":1000000,"description":"The project's budget in hours, 0–1000000 with at most two decimals; `null` removes it. The app compares the hours spent against it; `GET /projects/{id}/hours` reports the same."},"deactivated":{"type":"boolean","description":"Deactivate (`true`) or reactivate (`false`) the project, as the app does: a deactivated project takes no new bookings, moves to the end of the list, and stops being anyone's default task. Its bookings stay. Omit to leave it as it is; a new project is active."},"templateProjectId":{"type":"string","format":"uuid","description":"Create from this project as a template, as the app does: its members with their project roles and its task tree are copied, and its customer and (unless archived) its rate apply when the body names none. Without it the project has no members and no tasks, since a key is not a person who could be its first member; someone with project management rights finishes the setup in the app. A project this organization does not have is `422` `template_not_found`."}},"required":["name"],"additionalProperties":false,"description":"A new project. Unknown properties are rejected. To link it to your own id, create it with `PUT /projects/external/{externalId}` instead.","example":{"name":"Customer portal","projectNumber":"P-2026-001","customerExternalId":"K-1001","plannedHours":120}},"ProjectHours":{"type":"object","properties":{"projectId":{"type":"string","format":"uuid","description":"The project these hours belong to."},"plannedHours":{"type":["number","null"],"description":"The project's budget in hours, as `plannedHours` on `GET /projects/{id}`; null if none is set."},"spentHours":{"type":"number","minimum":0,"description":"Every booking on the project's tasks, billable or not, over the project's whole life. In decimal hours, each booking's `durationHours` summed and rounded to 3 decimals, so it matches summing that field over the project's bookings on `GET /time-bookings`."},"billableHours":{"type":"number","minimum":0,"description":"The billable part of `spentHours`. It splits into three parts that do not overlap: open, `readyForBillingHours` and `billedHours`. In decimal hours, each booking's `durationHours` summed and rounded to 3 decimals, so it matches summing that field over the project's bookings on `GET /time-bookings`."},"readyForBillingHours":{"type":"number","minimum":0,"description":"Billable bookings marked ready for billing and not billed yet. In decimal hours, each booking's `durationHours` summed and rounded to 3 decimals, so it matches summing that field over the project's bookings on `GET /time-bookings`."},"billedHours":{"type":"number","minimum":0,"description":"Billable bookings marked as billed. In decimal hours, each booking's `durationHours` summed and rounded to 3 decimals, so it matches summing that field over the project's bookings on `GET /time-bookings`."},"runningBookings":{"type":"integer","minimum":0,"description":"Bookings still running (a timer with no end yet). They count 0 in every sum above until they end, as `durationHours` is null for them."}},"required":["projectId","plannedHours","spentHours","billableHours","readyForBillingHours","billedHours","runningBookings"],"description":"A project's hours, summed live from its bookings when the request is made: what an ERP compares against the budget it set. Not part of the `Project` record, so recording time changes neither the project's `updatedAt` nor its `ETag`. Conditional requests work here too: the `ETag` changes when any sum does.","example":{"projectId":"77777777-7777-4777-8777-777777777777","plannedHours":120,"spentHours":86.5,"billableHours":80,"readyForBillingHours":12.25,"billedHours":60,"runningBookings":1}},"ProjectPage":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Project"},"description":"The records on this page, in cursor order."},"pagination":{"$ref":"#/components/schemas/Pagination"}},"required":["data","pagination"],"description":"One page of projects, oldest first. Pass `pagination.nextCursor` back as `cursor` and stop when `hasMore` is false; there is no total count.","example":{"data":[{"id":"77777777-7777-4777-8777-777777777777","externalId":"P-2026-001","name":"Customer portal","projectNumber":"P-2026-001","color":"#228BE6","colorSet":true,"deactivated":false,"customer":{"id":"88888888-8888-4888-8888-888888888888","name":"Example GmbH"},"rate":{"id":"aa000002-0000-4000-8000-000000000002","amount":135,"name":"Portal-Pauschale"},"plannedHours":120,"createdBy":{"type":"api_key","id":"44444444-4444-4444-8444-444444444444","name":"Nori Abrechnung"},"updatedBy":{"type":"api_key","id":"44444444-4444-4444-8444-444444444444","name":"Nori Abrechnung"},"updatedAt":"2026-08-14T09:05:00.000Z"}],"pagination":{"nextCursor":null,"hasMore":false}}},"ProjectPatchRequest":{"type":"object","properties":{"name":{"type":"string","minLength":3,"maxLength":255,"description":"Project display name, 3–255 characters after trimming."},"projectNumber":{"type":["string","null"],"minLength":1,"maxLength":64,"description":"Your project reference, 1–64 characters after trimming; `null` clears it. Not unique: the app does not require it to be."},"color":{"type":["string","null"],"pattern":"^#[0-9a-fA-F]{6}$","description":"Board color as `#RRGGBB` (stored lower-case); `null` returns the project to the stable color derived from its id, which is what `color` on the read reports then, with `colorSet` false. Sending that derived color back unchanged is not a change and does not pin it."},"customerId":{"type":["string","null"],"format":"uuid","description":"The customer, by TimeFusion id; `null` removes it. Cannot be combined with `customerExternalId`. An id this organization does not have is `422` `customer_not_found`."},"customerExternalId":{"type":["string","null"],"pattern":"^[\\x21-\\x7e](?:[\\x20-\\x7e]{0,126}[\\x21-\\x7e])?$","example":"K-1001","description":"The customer, by the `externalId` your system linked it under; `null` removes it, as `customerId: null` does. Cannot be combined with `customerId`. No customer linked to it is `422` `customer_not_found`: send the customer first. There is no find-or-create by name here, since that would need `customers:write`."},"rateId":{"type":["string","null"],"format":"uuid","description":"The hourly rate stored on this record, by id from `GET /rates`; `null` clears it. Cannot be combined with `rateExternalId`. Requires `money:read` and the organization's accounting feature, for clearing too (`403` `money_unavailable` otherwise). An archived rate cannot be newly assigned (`422` `rate_archived`), but sending the one already stored is not a change and is accepted."},"rateExternalId":{"type":["string","null"],"pattern":"^[\\x21-\\x7e](?:[\\x20-\\x7e]{0,126}[\\x21-\\x7e])?$","example":"K-1001","description":"The hourly rate, by the `externalId` your system linked it under (`PUT /rates/external/{externalId}`); `null` clears it, as `rateId: null` does. Cannot be combined with `rateId`. The same money gate and archived rule as `rateId`. No rate linked to it is `422` `rate_not_found`: send the rate first."},"plannedHours":{"type":["number","null"],"minimum":0,"maximum":1000000,"description":"The project's budget in hours, 0–1000000 with at most two decimals; `null` removes it. The app compares the hours spent against it; `GET /projects/{id}/hours` reports the same."},"deactivated":{"type":"boolean","description":"Deactivate (`true`) or reactivate (`false`) the project, as the app does: a deactivated project takes no new bookings, moves to the end of the list, and stops being anyone's default task. Its bookings stay. Omit to leave it as it is; a new project is active."},"externalId":{"type":["string","null"],"pattern":"^[\\x21-\\x7e](?:[\\x20-\\x7e]{0,126}[\\x21-\\x7e])?$","example":"K-1001","description":"Your system's identifier for this record: 1–128 printable ASCII characters, no leading or trailing spaces, case-sensitive. Unique per organization and resource; the upsert addresses records by it. `null` unlinks the record, which is how a client re-links: clear it here, then set it on the other record. Omit to leave it as it is."}},"additionalProperties":false,"description":"The fields to change. Merge semantics: an omitted field is left as stored, `null` clears a nullable one. A body that changes nothing writes nothing, so `updatedAt` and the `ETag` stay as they were. Unknown properties are rejected.","example":{"deactivated":true}},"ProjectRef":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Stable project identifier, usable as the `projectId` filter."},"name":{"type":"string","description":"Project display name."},"projectNumber":{"type":["string","null"],"description":"Organization-defined project reference, or null if unset."}},"required":["id","name","projectNumber"],"description":"A project, as referenced from another resource.","example":{"id":"77777777-7777-4777-8777-777777777777","name":"Customer portal","projectNumber":"P-2026-001"}},"ProjectUpsertRequest":{"type":"object","properties":{"name":{"type":"string","minLength":3,"maxLength":255,"description":"Project display name, 3–255 characters after trimming."},"projectNumber":{"type":["string","null"],"minLength":1,"maxLength":64,"description":"Your project reference, 1–64 characters after trimming; `null` clears it. Not unique: the app does not require it to be."},"color":{"type":["string","null"],"pattern":"^#[0-9a-fA-F]{6}$","description":"Board color as `#RRGGBB` (stored lower-case); `null` returns the project to the stable color derived from its id, which is what `color` on the read reports then, with `colorSet` false. Sending that derived color back unchanged is not a change and does not pin it."},"customerId":{"type":["string","null"],"format":"uuid","description":"The customer, by TimeFusion id; `null` removes it. Cannot be combined with `customerExternalId`. An id this organization does not have is `422` `customer_not_found`."},"customerExternalId":{"type":["string","null"],"pattern":"^[\\x21-\\x7e](?:[\\x20-\\x7e]{0,126}[\\x21-\\x7e])?$","example":"K-1001","description":"The customer, by the `externalId` your system linked it under; `null` removes it, as `customerId: null` does. Cannot be combined with `customerId`. No customer linked to it is `422` `customer_not_found`: send the customer first. There is no find-or-create by name here, since that would need `customers:write`."},"rateId":{"type":["string","null"],"format":"uuid","description":"The hourly rate stored on this record, by id from `GET /rates`; `null` clears it. Cannot be combined with `rateExternalId`. Requires `money:read` and the organization's accounting feature, for clearing too (`403` `money_unavailable` otherwise). An archived rate cannot be newly assigned (`422` `rate_archived`), but sending the one already stored is not a change and is accepted."},"rateExternalId":{"type":["string","null"],"pattern":"^[\\x21-\\x7e](?:[\\x20-\\x7e]{0,126}[\\x21-\\x7e])?$","example":"K-1001","description":"The hourly rate, by the `externalId` your system linked it under (`PUT /rates/external/{externalId}`); `null` clears it, as `rateId: null` does. Cannot be combined with `rateId`. The same money gate and archived rule as `rateId`. No rate linked to it is `422` `rate_not_found`: send the rate first."},"plannedHours":{"type":["number","null"],"minimum":0,"maximum":1000000,"description":"The project's budget in hours, 0–1000000 with at most two decimals; `null` removes it. The app compares the hours spent against it; `GET /projects/{id}/hours` reports the same."},"deactivated":{"type":"boolean","description":"Deactivate (`true`) or reactivate (`false`) the project, as the app does: a deactivated project takes no new bookings, moves to the end of the list, and stops being anyone's default task. Its bookings stay. Omit to leave it as it is; a new project is active."},"templateProjectId":{"type":"string","format":"uuid","description":"Create from this project as a template, as the app does: its members with their project roles and its task tree are copied, and its customer and (unless archived) its rate apply when the body names none. Without it the project has no members and no tasks, since a key is not a person who could be its first member; someone with project management rights finishes the setup in the app. A project this organization does not have is `422` `template_not_found`. Applied only when this request creates the project; an update ignores it."}},"required":["name"],"additionalProperties":false,"description":"The project linked to the path's `externalId`, as it should be. Merge semantics: an omitted field is left as stored on an update, `null` clears a nullable one. `name` is required either way, because the upsert may create. Unknown properties are rejected.","example":{"name":"Customer portal","projectNumber":"P-2026-001","customerExternalId":"K-1001","plannedHours":120}},"Rate":{"type":"object","properties":{"id":{"type":["string","null"],"format":"uuid","description":"Stable identifier of the `HourlyRate` row the live rate was resolved from. Null on a frozen rate, for the same reason `source` is: the accounting snapshot records an amount and a label, not an id, and the level that won may have been edited or reassigned since billing, so any id reported here could disagree with the amount beside it."},"amount":{"type":"number","description":"The hourly rate, in the organization's currency — see `organization.currency` on `GET /me`."},"name":{"type":["string","null"],"description":"Label of the rate, e.g. \"Standard\", or null if unset."},"source":{"type":["string","null"],"description":"Which level of the rate chain supplied the rate, or null on a frozen rate: the snapshot records an amount and a label, not the level it came from. The chain is walked in the order below and the first level holding a rate wins. `booking`: An override stored on this booking alone, which is why it beats every other level: one special-cased line can be priced without touching a rate other bookings share. `task`: The booked task's own rate, published as `Task.rate`. `project`: The rate stored on the booked task's project, published as `Project.rate`. A project or customer rate is a price agreed for that work, which is why both outrank the personal rate below. `customer`: The rate stored on the customer of the booked task's project, published as `Customer.rate`, applying to every project of that customer without a rate of its own. `user`: The rate stored on the person who recorded the booking, published as `User.rate`. It is the last level before the organization default, so it applies only where no booking, task, project or customer rate is set. `org`: The organization's default rate, the entry of `GET /rates` with `isOrganizationDefault: true`."},"frozen":{"type":"boolean","description":"True when the booking bills the snapshot frozen at billing time, so a later edit to the underlying rate cannot change it. False means the rate is resolved live and can still move."}},"required":["id","amount","name","source","frozen"],"description":"The rate applied to this booking. Null when the booking is not billable, when no rate resolves (unpriced), or when the key may not see money.","example":{"id":"aa000001-0000-4000-8000-000000000001","amount":120,"name":"Standard","source":"task","frozen":false}},"RateBatchRequest":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"externalId":{"type":"string","pattern":"^[\\x21-\\x7e](?:[\\x20-\\x7e]{0,126}[\\x21-\\x7e])?$","example":"K-1001","description":"Your system's identifier for this record: 1–128 printable ASCII characters, no leading or trailing spaces, case-sensitive. Unique per organization and resource; the upsert addresses records by it."},"ifMatch":{"type":"string","pattern":"^(?:\\*|(?:W\\/)?\"[\\x21\\x23-\\x7e]*\")$","example":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\"","description":"Optional: this item's `If-Match`. The `ETag` of the record as you last read or wrote it, or `*` to update only; the item is refused with `precondition_failed` when the record linked to its `externalId` no longer has exactly that representation for this key, or (with either value) when none is linked. A weak tag counts as the strong one it was; anything malformed is `400`."},"name":{"type":"string","minLength":1,"maxLength":255,"description":"Label of the rate, 1–255 characters after trimming. Not unique: the app allows two rates of one name, so a sync addresses rates by `externalId`, never by name."},"description":{"type":["string","null"],"minLength":1,"maxLength":2000,"description":"Optional note saying what the rate is for, 1–2000 characters after trimming; `null` clears it."},"amount":{"type":"number","minimum":0,"maximum":100000,"description":"The rate per hour in the organization's currency (`organization.currency` on `GET /me`), 0–100000 with at most two decimals. 0 is a valid rate, for internal or pro-bono work. **Changing it re-prices every booking not yet billed that resolves to this rate**, as an edit in the app does: those bookings read the rate live, and their `billingRevision` changes. Billed bookings keep the amount they were billed at."},"archived":{"type":"boolean","description":"Archive (`true`) or reactivate (`false`) the rate, as the app does: an archived rate can no longer be assigned, but stays on every record already using it, the organization default included. Omit to leave it as it is; a new rate is active."}},"required":["externalId","name","amount"],"additionalProperties":false,"description":"One rate, as `PUT /rates/external/{externalId}` takes it, with the path's `externalId` and the `If-Match` header as fields. Unknown properties are rejected."},"minItems":1,"maxItems":100,"description":"1–100 rates, each upserted by its `externalId` exactly as `PUT /rates/external/{externalId}` upserts one, in this order. Each `externalId` at most once per batch."},"dryRun":{"type":"boolean","default":false,"description":"Run the whole batch and report what it would do, then write nothing. Every refusal a real call meets is met, so a dry run answers the same `200`, `403` or `422` a real call would. A dry run takes no `Idempotency-Key`."},"adoptExisting":{"type":"boolean","default":false,"description":"`adoptExisting` of the single upsert, for every item: when no record is linked to an item's `externalId` yet, link an existing unlinked one instead of creating a new one. Off by default."}},"required":["items"],"additionalProperties":false,"description":"Hourly rates to upsert by `externalId` in one all-or-nothing transaction. Unknown properties are rejected.","example":{"items":[{"externalId":"PL-STD","name":"Standard","amount":120},{"externalId":"PL-SEN","name":"Senior Consulting","amount":150}],"adoptExisting":true}},"RateBatchResult":{"type":"object","properties":{"dryRun":{"type":"boolean","description":"Whether this response describes a simulation. `false` means the items were written; `true` means nothing was."},"created":{"type":"integer","minimum":0,"description":"How many rates were created."},"updated":{"type":"integer","minimum":0,"description":"How many existing rates were changed, adoptions included (linking writes the `externalId`)."},"unchanged":{"type":"integer","minimum":0,"description":"How many rates already matched their item, so nothing was written for them."},"items":{"type":"array","items":{"type":"object","properties":{"externalId":{"type":"string","description":"The item's `externalId`, as sent."},"outcome":{"type":"string","enum":["created","updated","unchanged"],"description":"`created`: no record was linked and none adopted, so one was created. `updated`: a linked or adopted record was changed. `unchanged`: the linked record already matched, so nothing was written and its `updatedAt` and `ETag` stayed put."},"id":{"type":["string","null"],"format":"uuid","description":"The record's id. `null` only on a dry run's `created` item, whose record does not exist."},"record":{"allOf":[{"$ref":"#/components/schemas/RateCatalogEntry"},{"type":["object","null"],"description":"The record as `GET` by id returns it for this key, as it now is. `null` on a dry run."}]},"etag":{"type":["string","null"],"description":"The record's `ETag`, as the single write's `ETag` header would carry it: store it to send as this item's `ifMatch` next time. `null` on a dry run."}},"required":["externalId","outcome","id","record","etag"]},"description":"One entry per item, in request order."}},"required":["dryRun","created","updated","unchanged","items"],"description":"What a rate batch did, or what a dry run would have done. `created + updated + unchanged` always equals the number of items, and `items` answers them in request order.","example":{"dryRun":false,"created":0,"updated":1,"unchanged":0,"items":[{"externalId":"PL-STD","outcome":"updated","id":"aa000001-0000-4000-8000-000000000001","record":{"id":"aa000001-0000-4000-8000-000000000001","externalId":"PL-STD","name":"Standard","description":"Standard for tracked work.","amount":120,"archived":false,"isOrganizationDefault":true,"createdBy":{"type":"user","id":"66666666-6666-4666-8666-666666666666","name":"Alex Example"},"updatedBy":{"type":"api_key","id":"44444444-4444-4444-8444-444444444444","name":"Nori Abrechnung"},"updatedAt":"2026-08-14T09:05:00.000Z"},"etag":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\""}]}},"RateCatalogEntry":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Stable identifier of the stored `HourlyRate` row. This is the value a task, project, customer or person `rate.id` and a booking `pricing.rate.id` name, so it is the join key between the master data and this catalog."},"externalId":{"type":["string","null"],"description":"Your system's identifier for this rate, as an integration set it through this API; null when the rate is not linked. Unique per organization among rates, case-sensitive, and shown read-only in the app. Find a rate by it with `?externalId=`, or write it with the upsert."},"name":{"type":"string","description":"Label of the rate, e.g. \"Standard\". Renaming a rate changes this and leaves the `id` alone, which is why the `id` is the value to store."},"description":{"type":["string","null"],"description":"Optional note stored on the rate, saying what it is for, or null if none was entered. Never a booking's label: an accounted booking can freeze a rate name that no longer matches this one."},"amount":{"type":["number","null"],"description":"The stored rate per hour, in the organization's currency; see `organization.currency` on `GET /me`. **Null when the key may not see money** (`money.visible` on `GET /me`), while the entry and its other fields stay visible: this catalog exists to resolve ids and names, and a name is not money. Unlike an embedded `HourlyRate` on a task, project, customer or person, which is null as a whole when money is hidden, the `id` remains readable here so a rate an integrator already references still resolves."},"archived":{"type":"boolean","description":"An archived rate is hidden from new pickers in the app and cannot be assigned; records already using it keep it and still report its `id`. Returned here regardless, flagged — filter with `?archived=`."},"isOrganizationDefault":{"type":"boolean","description":"True for the one rate the organization bills with when no other level of the chain applies — the rate `Organization.defaultHourlyRateId` points to, the last link of the chain a booking resolves through. Changing the default writes the organization, not this rate, so this can flip without the entry's `updatedAt` moving; the response's `ETag` still changes, because it covers the body."},"createdBy":{"anyOf":[{"$ref":"#/components/schemas/RecordActorUser"},{"$ref":"#/components/schemas/RecordActorApiKey"},{"type":"null"}],"description":"Who created this rate. Null when unknown: a rate created before this was recorded, or one whose creator has since been deleted."},"updatedBy":{"anyOf":[{"$ref":"#/components/schemas/RecordActorUser"},{"$ref":"#/components/schemas/RecordActorApiKey"},{"type":"null"}],"description":"Who last changed a published field of this rate; for a new rate, its creator. Null when unknown, as for `createdBy`. To recognize a change you made yourself, compare `updatedBy.id` with `key.id` on `GET /me` **and** `updatedAt` with the one your write returned: a key rotation changes the id, and a reorder in the app moves `updatedAt` without moving `updatedBy`."},"updatedAt":{"type":"string","format":"date-time","description":"When this rate row was last written, in UTC; `?sort=updatedAt` orders by it. Upsert on it rather than comparing whole bodies. It does **not** advance when the rate becomes or stops being the organization default: that writes the organization row, not this one, and `isOrganizationDefault` here is where the change shows up. The response's `ETag` still changes, because it covers the body.","example":"2026-08-14T07:30:00.000Z"}},"required":["id","externalId","name","description","amount","archived","isOrganizationDefault","createdBy","updatedBy","updatedAt"],"description":"An hourly rate stored in the organization, whatever uses it: this is the rate catalog `GET /rates` pages and the target of every `rate.id` the API publishes. Not a booking's billed rate — that is `Rate`, which records how one booking resolved its price.","example":{"id":"aa000001-0000-4000-8000-000000000001","externalId":"PL-STD","name":"Standard","description":"Standard for tracked work.","amount":120,"archived":false,"isOrganizationDefault":true,"createdBy":{"type":"user","id":"66666666-6666-4666-8666-666666666666","name":"Alex Example"},"updatedBy":{"type":"api_key","id":"44444444-4444-4444-8444-444444444444","name":"Nori Abrechnung"},"updatedAt":"2026-08-14T09:05:00.000Z"}},"RateCatalogEntryPage":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/RateCatalogEntry"},"description":"The records on this page, in cursor order."},"pagination":{"$ref":"#/components/schemas/Pagination"}},"required":["data","pagination"],"description":"One page of hourly rates, oldest first. Pass `pagination.nextCursor` back as `cursor` and stop when `hasMore` is false; there is no total count.","example":{"data":[{"id":"aa000001-0000-4000-8000-000000000001","externalId":"PL-STD","name":"Standard","description":"Standard for tracked work.","amount":120,"archived":false,"isOrganizationDefault":true,"createdBy":{"type":"user","id":"66666666-6666-4666-8666-666666666666","name":"Alex Example"},"updatedBy":{"type":"api_key","id":"44444444-4444-4444-8444-444444444444","name":"Nori Abrechnung"},"updatedAt":"2026-08-14T09:05:00.000Z"}],"pagination":{"nextCursor":null,"hasMore":false}}},"RateCreateRequest":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255,"description":"Label of the rate, 1–255 characters after trimming. Not unique: the app allows two rates of one name, so a sync addresses rates by `externalId`, never by name."},"description":{"type":["string","null"],"minLength":1,"maxLength":2000,"description":"Optional note saying what the rate is for, 1–2000 characters after trimming; `null` clears it."},"amount":{"type":"number","minimum":0,"maximum":100000,"description":"The rate per hour in the organization's currency (`organization.currency` on `GET /me`), 0–100000 with at most two decimals. 0 is a valid rate, for internal or pro-bono work. **Changing it re-prices every booking not yet billed that resolves to this rate**, as an edit in the app does: those bookings read the rate live, and their `billingRevision` changes. Billed bookings keep the amount they were billed at."},"archived":{"type":"boolean","description":"Archive (`true`) or reactivate (`false`) the rate, as the app does: an archived rate can no longer be assigned, but stays on every record already using it, the organization default included. Omit to leave it as it is; a new rate is active."}},"required":["name","amount"],"additionalProperties":false,"description":"A new hourly rate. Unknown properties are rejected. To link it to your own id, create it with `PUT /rates/external/{externalId}` instead.","example":{"name":"Senior Consulting","amount":150}},"RatePatchRequest":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255,"description":"Label of the rate, 1–255 characters after trimming. Not unique: the app allows two rates of one name, so a sync addresses rates by `externalId`, never by name."},"description":{"type":["string","null"],"minLength":1,"maxLength":2000,"description":"Optional note saying what the rate is for, 1–2000 characters after trimming; `null` clears it."},"amount":{"type":"number","minimum":0,"maximum":100000,"description":"The rate per hour in the organization's currency (`organization.currency` on `GET /me`), 0–100000 with at most two decimals. 0 is a valid rate, for internal or pro-bono work. **Changing it re-prices every booking not yet billed that resolves to this rate**, as an edit in the app does: those bookings read the rate live, and their `billingRevision` changes. Billed bookings keep the amount they were billed at."},"archived":{"type":"boolean","description":"Archive (`true`) or reactivate (`false`) the rate, as the app does: an archived rate can no longer be assigned, but stays on every record already using it, the organization default included. Omit to leave it as it is; a new rate is active."},"externalId":{"type":["string","null"],"pattern":"^[\\x21-\\x7e](?:[\\x20-\\x7e]{0,126}[\\x21-\\x7e])?$","example":"K-1001","description":"Your system's identifier for this record: 1–128 printable ASCII characters, no leading or trailing spaces, case-sensitive. Unique per organization and resource; the upsert addresses records by it. `null` unlinks the record, which is how a client re-links: clear it here, then set it on the other record. Omit to leave it as it is."}},"additionalProperties":false,"description":"The fields to change. Merge semantics: an omitted field is left as stored, `null` clears a nullable one. A body that changes nothing writes nothing, so `updatedAt` and the `ETag` stay as they were. Unknown properties are rejected.","example":{"amount":155}},"RateUpsertRequest":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255,"description":"Label of the rate, 1–255 characters after trimming. Not unique: the app allows two rates of one name, so a sync addresses rates by `externalId`, never by name."},"description":{"type":["string","null"],"minLength":1,"maxLength":2000,"description":"Optional note saying what the rate is for, 1–2000 characters after trimming; `null` clears it."},"amount":{"type":"number","minimum":0,"maximum":100000,"description":"The rate per hour in the organization's currency (`organization.currency` on `GET /me`), 0–100000 with at most two decimals. 0 is a valid rate, for internal or pro-bono work. **Changing it re-prices every booking not yet billed that resolves to this rate**, as an edit in the app does: those bookings read the rate live, and their `billingRevision` changes. Billed bookings keep the amount they were billed at."},"archived":{"type":"boolean","description":"Archive (`true`) or reactivate (`false`) the rate, as the app does: an archived rate can no longer be assigned, but stays on every record already using it, the organization default included. Omit to leave it as it is; a new rate is active."}},"required":["name","amount"],"additionalProperties":false,"description":"The rate linked to the path's `externalId`, as it should be. Merge semantics: an omitted field is left as stored on an update, `null` clears a nullable one. `name` and `amount` are required either way, because the upsert may create. Unknown properties are rejected.","example":{"name":"Senior Consulting","amount":150}},"RecordActorApiKey":{"type":"object","properties":{"type":{"type":"string","enum":["api_key"],"description":"An API key, through this API."},"id":{"type":"string","format":"uuid","description":"Stable identifier of the API key; compare it with `key.id` on `GET /me` to recognize your own writes."},"name":{"type":"string","description":"Display name of the API key."}},"required":["type","id","name"],"description":"An API key that wrote the record through this API.","example":{"type":"api_key","id":"44444444-4444-4444-8444-444444444444","name":"Nori Abrechnung"}},"RecordActorUser":{"type":"object","properties":{"type":{"type":"string","enum":["user"],"description":"A person, in the app."},"id":{"type":"string","format":"uuid","description":"Stable user identifier, the same value `GET /users` returns."},"name":{"type":["string","null"],"description":"Person's display name, or null if unset."}},"required":["type","id","name"],"description":"A person who wrote the record in the app.","example":{"type":"user","id":"66666666-6666-4666-8666-666666666666","name":"Alex Example"}},"Task":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Stable task identifier, usable as the `taskId` filter."},"name":{"type":"string","description":"Task display name. This node's own name only, never a joined path; the ancestors' names are in `path`, which is what a breadcrumb renders from."},"project":{"anyOf":[{"$ref":"#/components/schemas/ProjectRef"},{"type":"null"}],"description":"The project this task belongs to, as the same `ProjectRef` a booking carries. Always set in practice: the field is nullable only because the underlying column is, and every task has a project."},"kind":{"type":["string","null"],"description":"This task's own level in the tree: the same value the last element of `path` carries, and the value the `kind` filter matches. Today `subArea`, `serviceArea` or `activity`, and null for a flat task that sits in no tree. An open vocabulary, not a closed set; read it as a label rather than an enum, and expect values beyond these three."},"bookable":{"type":"boolean","description":"Whether time can be recorded on this task; the same value the last element of `path` carries. A task with `false` is a grouping node: it organizes the tree and takes no time of its own. Not the same claim as “is a leaf”, see `TaskPath`."},"parentTaskId":{"type":["string","null"],"format":"uuid","description":"Id of the parent task, for rebuilding the tree client-side. **Null means this task is a root**, either the top of a tree or a task that sits in none, so a null here is the terminating case rather than a missing value. Whenever `path` has more than one segment this is the id of the segment above the last. For a single task's chain, read `path` instead of rebuilding."},"path":{"$ref":"#/components/schemas/TaskPath"},"billableByDefault":{"type":"boolean","description":"Whether new bookings on this task start out billable. A default only: whether an existing booking is billable is `pricing.billable` on `TimeBooking`, which can differ and is what decides whether that booking is priced. An unset value is reported as `false`, which is how the app treats it."},"rate":{"anyOf":[{"$ref":"#/components/schemas/HourlyRate"},{"type":"null"}],"description":"The task's own hourly rate: one link of the chain that prices a booking (a rate on the booking itself, then this one, then the project's, then the customer's, then the person's, then the organization default, first one set wins), not necessarily the rate any given booking bills. Null when no rate is stored on the task, and when the key may not see money (see `money.visible` on `GET /me`). A rate archived in the app stays attached to the tasks already using it and is still reported here."},"updatedAt":{"type":"string","format":"date-time","description":"When this task row was last written, in UTC; `?sort=updatedAt` orders by it. Upsert on it rather than comparing whole bodies. It does **not** advance when an ancestor in `path`, or the `project`, is renamed on its own row — every segment but the last is another task's name, and a renamed ancestor shows up as that task's own `updatedAt` moving. It also does **not** advance when this task's `rate` is edited: a rate's `name` and `amount` live on the rate itself, so changing either moves the rate's own `updatedAt` and leaves this task row untouched; `GET /rates` is where that change shows up. Assigning a different rate, or clearing it, does move it: which rate applies is a column on this row. The response's `ETag` still changes, because it covers the body.","example":"2026-08-14T07:30:00.000Z"}},"required":["id","name","project","kind","bookable","parentTaskId","path","billableByDefault","rate","updatedAt"],"description":"A task in a project's tree, as the thing time is booked against. It carries the resolved `path` from the tree's root down to itself, so a breadcrumb needs no second request, and its own `rate`, which is one link of the chain that prices a booking rather than the rate any booking bills.","example":{"id":"33333333-3333-4333-8333-333333333333","name":"Implementation","project":{"id":"77777777-7777-4777-8777-777777777777","name":"Customer portal","projectNumber":"P-2026-001"},"kind":"activity","bookable":true,"parentTaskId":"22222222-2222-4222-8222-222222222222","path":[{"id":"11111111-1111-4111-8111-111111111111","name":"Engineering","bookable":false,"kind":"subArea","order":1},{"id":"22222222-2222-4222-8222-222222222222","name":"Development","bookable":false,"kind":"serviceArea","order":1},{"id":"33333333-3333-4333-8333-333333333333","name":"Implementation","bookable":true,"kind":"activity","order":1}],"billableByDefault":true,"rate":{"id":"aa000001-0000-4000-8000-000000000001","amount":120,"name":"Standard"},"updatedAt":"2026-08-14T09:05:00.000Z"}},"TaskPage":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Task"},"description":"The records on this page, in cursor order."},"pagination":{"$ref":"#/components/schemas/Pagination"}},"required":["data","pagination"],"description":"One page of tasks, oldest first. Pass `pagination.nextCursor` back as `cursor` and stop when `hasMore` is false; there is no total count.","example":{"data":[{"id":"33333333-3333-4333-8333-333333333333","name":"Implementation","project":{"id":"77777777-7777-4777-8777-777777777777","name":"Customer portal","projectNumber":"P-2026-001"},"kind":"activity","bookable":true,"parentTaskId":"22222222-2222-4222-8222-222222222222","path":[{"id":"11111111-1111-4111-8111-111111111111","name":"Engineering","bookable":false,"kind":"subArea","order":1},{"id":"22222222-2222-4222-8222-222222222222","name":"Development","bookable":false,"kind":"serviceArea","order":1},{"id":"33333333-3333-4333-8333-333333333333","name":"Implementation","bookable":true,"kind":"activity","order":1}],"billableByDefault":true,"rate":{"id":"aa000001-0000-4000-8000-000000000001","amount":120,"name":"Standard"},"updatedAt":"2026-08-14T09:05:00.000Z"}],"pagination":{"nextCursor":null,"hasMore":false}}},"TaskPath":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Stable identifier of this task-tree node."},"name":{"type":"string","description":"Display name of this task-tree node."},"bookable":{"type":"boolean","description":"Whether time can be recorded **on this node**. Not the same claim as “is a leaf”: today only the deepest node of a structured tree and a flat task accept bookings, but an inner node becoming bookable is a change in this value, never a change in this contract. Branch on this field, not on the segment's position."},"kind":{"type":["string","null"],"description":"The organization's own name for this level of the tree — today `subArea`, `serviceArea` or `activity`, and null for a flat task. An open vocabulary, not a closed set: it describes how one organization structures its work, so read it as a label and do not assume these three values are the only ones."},"order":{"type":"integer","description":"Sibling sort order. Always present; a task with no order stored reports `0`."}},"required":["id","name","bookable","kind","order"]},"minItems":1,"description":"The task's position in its project's task tree, root first, **including the task itself as the last element** — so `path[path.length - 1].id` always equals the task's own id and a breadcrumb renders from this field alone. Never null and never empty: a task outside any tree is a path of one. At most 6 segments.","example":[{"id":"11111111-1111-4111-8111-111111111111","name":"Engineering","bookable":false,"kind":"subArea","order":1},{"id":"22222222-2222-4222-8222-222222222222","name":"Development","bookable":false,"kind":"serviceArea","order":1},{"id":"33333333-3333-4333-8333-333333333333","name":"Implementation","bookable":true,"kind":"activity","order":1}]},"TimeBooking":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Stable booking identifier; use it in billing requests."},"kind":{"type":"string","description":"`tracked` is a real begin/end range; `plain` is a duration-only \"plain hours\" entry with no meaningful `endAt`."},"beginAt":{"type":"string","format":"date-time","description":"Booking start in UTC; the booking date for a duration-only entry.","example":"2026-08-14T07:30:00.000Z"},"endAt":{"type":["string","null"],"format":"date-time","description":"Booking end in UTC. Null for a still-running booking, and for `plain` entries, which carry no end time.","example":"2026-08-14T07:30:00.000Z"},"durationMinutes":{"type":["integer","null"],"description":"Duration in whole minutes. Null means a tracked booking is still running; such bookings are available only through `GET /time-bookings/{id}` and excluded from lists. Do not sum null as zero. Not rounded by any organization rounding rule; this is the raw value."},"durationHours":{"type":["number","null"],"description":"Duration in decimal hours, rounded to 3 decimals, with no organization rounding rule applied: 90 minutes is `1.5`. Use it to show hours instead of dividing `durationMinutes` yourself; amounts are computed from whole minutes, not from this field. A `tracked` booking computes it from the exact begin/end range, so it can differ slightly from `durationMinutes / 60`; for a `plain` entry it is exactly that. Null while a booking is still running, `0` when the end is before the start."},"analysis":{"type":"object","properties":{"status":{"type":"string","description":"Stored analysis state. `notApplicable`: Plain-hour entry; excluded from analysis. `pending`: No stored analysis timestamp; not a successful check or a promise that calculation is queued. `ok`: Analysis completed without a booking error. `error`: Analysis completed with a booking error."},"errorMessage":{"type":["string","null"],"description":"English display message for an analyzed booking error, and null on every status but `error`. Show it to a person and branch on `errorCode` instead: this sentence is written for a reader and may be reworded in any release."},"errorCode":{"type":["string","null"],"description":"Which booking error the stored analysis found. Null on every status but `error`, so it does not by itself separate \"analyzed, found nothing\" from \"never analyzed\": read `status` for that. Plain-hour entries are never analyzed and report `notApplicable`. Each code is reported with a fixed `errorMessage`. `outsideOfBoundsPartial`: Booking lies partly outside recorded clock-in/clock-out periods. `outsideOfBounds`: Booking lies outside all recorded clock-in/clock-out periods. `overlapWithOther`: Booking overlaps another booking. `missingEnd`: Booking has no end time. `endBeforeBegin`: Booking ends before it starts."},"calculatedAt":{"type":["string","null"],"format":"date-time","description":"When the booking analysis was last calculated, in UTC. Null when no analysis result is stored, including for plain-hour entries, which are never analyzed. Reading this API does not recalculate it.","example":"2026-08-14T07:30:00.000Z"}},"required":["status","errorMessage","errorCode","calculatedAt"],"description":"Stored booking analysis. Always present and never null: a plain-hour entry carries `status: \"notApplicable\"` rather than an absent object."},"comment":{"type":["string","null"],"description":"Booking comment entered by the employee, or null if unset."},"finalized":{"type":"boolean","description":"Whether the booking's recorded time is locked against editing by a period lock: the organization's change lock, or the owner's monthly-close lock when that workflow is enabled and the owner is not `monthlyCloseExempt`. Evaluated on `beginAt` in the organization's time zone (`organization.timeZone` on `GET /me`, Europe/Vienna today), for plain and running bookings too. Independent of `pricing.billable` and `billing.billed`. Current state, not a guarantee: reopening a period turns this back to `false`, and the booking becomes editable again unless it is billed or marked ready for billing (`billing.readyForBilling`), separate locks this field does not report."},"pricing":{"type":"object","properties":{"billable":{"type":"boolean","description":"Whether the booking may be invoiced to the customer."},"rate":{"anyOf":[{"$ref":"#/components/schemas/Rate"},{"type":"null"}],"description":"The rate this booking bills, once the chain in `Rate.source` has resolved. Null when the booking is not billable, when no rate resolves for it, or when the key may not see money; never `0` for any of those. Not the same field as `Task.rate`, `Project.rate`, `Customer.rate` or `User.rate`, each of which is one link of that chain rather than the outcome."},"amount":{"type":["number","null"],"description":"`pricing.rate.amount × durationMinutes / 60`, rounded to 2 decimals. Null when the booking is not billable, still running, unpriced, or the key may not see money; never `0` for any of those. Summing it over a set of bookings matches the in-app project report to the cent, unless the report has its time-rounding option on."}},"required":["billable","rate","amount"],"description":"Whether and how much the booking can be charged. Always present; rate and amount are null when unavailable."},"billing":{"type":"object","properties":{"billed":{"type":"boolean","description":"Whether the booking is marked as billed."},"at":{"type":["string","null"],"format":"date-time","description":"When the booking was marked as billed, in UTC; null when it is not billed.","example":"2026-08-14T07:30:00.000Z"},"by":{"anyOf":[{"$ref":"#/components/schemas/BilledBy"},{"type":"null"}],"description":"Who marked the booking as billed. Null when the booking is not billed."},"invoice":{"anyOf":[{"$ref":"#/components/schemas/InvoiceRef"},{"type":"null"}],"description":"The invoice record the booking is billed under; null when it is not billed."},"readyForBilling":{"type":["object","null"],"properties":{"at":{"type":"string","format":"date-time","description":"When the booking was marked ready for billing, in UTC.","example":"2026-08-14T07:30:00.000Z"},"by":{"anyOf":[{"$ref":"#/components/schemas/UserRef"},{"type":"null"}],"description":"The project admin who marked it. Null when that person's account was deleted since; the mark stands."}},"required":["at","by"],"description":"The project admin's release of this booking to accounting (**Abrechnungsbereit** in the app), or null when it is not marked. A marked booking is locked against editing in the app until the mark is lifted, and the mark stays after billing, so a billed booking can carry it too. Filter with `?readyForBilling=`, and require it on the billing write with `requireReadyForBilling`. Whether the organization uses the mark is `organization.readyForBillingEnabled` on `GET /me`."}},"required":["billed","at","by","invoice","readyForBilling"],"description":"Current billing state and invoice attribution. Always present."},"user":{"$ref":"#/components/schemas/UserRef"},"project":{"anyOf":[{"$ref":"#/components/schemas/ProjectRef"},{"type":"null"}],"description":"The project the booked `task` belongs to, read through that task rather than stored on the booking, so the two cannot disagree. Never null in practice — every booking this API returns is scoped to your organization through its project — but the underlying task→project link is optional, so the field is typed nullable rather than fabricating one."},"task":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Stable identifier of the booked task."},"name":{"type":"string","description":"Task display name."},"path":{"$ref":"#/components/schemas/TaskPath"}},"required":["id","name","path"],"description":"The task on which the work was recorded."},"updatedAt":{"type":"string","format":"date-time","description":"When this booking row was last written, in UTC; `?sort=updatedAt` orders by it. Advances on any write to the booking, including internal recalculation nobody edited. Does **not** advance when a name reached through `project`, `task.path` or `user` changes on that record's own row, nor when an unbilled booking's live `pricing.rate` and `pricing.amount` change because a rate in its chain was edited or reassigned. A row timestamp, not a commit watermark: a delayed commit can land behind your checkpoint, so resume with some overlap and periodically reconcile whole periods.","example":"2026-08-14T07:30:00.000Z"},"billingRevision":{"type":["string","null"],"description":"Opaque token that changes whenever this booking's billing inputs change. Save it with the booking you reviewed and send it back in `expectedBillingRevisions`; the write is refused if any input moved in the meantime. Inputs: kind and effective duration, `pricing.billable`, `billing.billed` and its invoice, the resolved rate and its snapshot, the assigned user, task, project and customer, `finalized`, and whether `billing.readyForBilling` is set (not when or by whom). The booking analysis is not an input: the billing write checks it directly. Display names are not inputs, so a rename does not change it. Null when the key may not see money or the server has no signing key configured; null means no review is possible, not that the booking cannot change."}},"required":["id","kind","beginAt","endAt","durationMinutes","durationHours","analysis","comment","finalized","pricing","billing","user","project","task","updatedAt","billingRevision"],"description":"One recorded piece of work: when it happened, who recorded it, what it is worth, and whether an invoice already carries it. The resource the invoicing loop selects from and `POST /time-bookings/billing` writes to. Creating and editing bookings stays in the app; this API reads them and marks them billed.","example":{"id":"55555555-5555-4555-8555-555555555555","kind":"tracked","beginAt":"2026-08-14T07:30:00.000Z","endAt":"2026-08-14T09:00:00.000Z","durationMinutes":90,"durationHours":1.5,"analysis":{"status":"ok","errorMessage":null,"errorCode":null,"calculatedAt":"2026-08-14T09:05:00.000Z"},"comment":"Implement invoice export","finalized":true,"pricing":{"billable":true,"rate":{"id":"aa000001-0000-4000-8000-000000000001","amount":120,"name":"Standard","source":"task","frozen":false},"amount":180},"billing":{"billed":false,"at":null,"by":null,"invoice":null,"readyForBilling":{"at":"2026-08-31T15:40:00.000Z","by":{"id":"bb000001-0000-4000-8000-000000000001","name":"Robin Example"}}},"user":{"id":"66666666-6666-4666-8666-666666666666","name":"Alex Example"},"project":{"id":"77777777-7777-4777-8777-777777777777","name":"Customer portal","projectNumber":"P-2026-001"},"task":{"id":"33333333-3333-4333-8333-333333333333","name":"Implementation","path":[{"id":"11111111-1111-4111-8111-111111111111","name":"Engineering","bookable":false,"kind":"subArea","order":1},{"id":"22222222-2222-4222-8222-222222222222","name":"Development","bookable":false,"kind":"serviceArea","order":1},{"id":"33333333-3333-4333-8333-333333333333","name":"Implementation","bookable":true,"kind":"activity","order":1}]},"updatedAt":"2026-08-14T09:05:00.000Z","billingRevision":null}},"TimeBookingPage":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/TimeBooking"},"description":"The records on this page, in cursor order."},"pagination":{"$ref":"#/components/schemas/Pagination"}},"required":["data","pagination"],"description":"One page of time bookings, oldest first. Pass `pagination.nextCursor` back as `cursor` and stop when `hasMore` is false; there is no total count.","example":{"data":[{"id":"55555555-5555-4555-8555-555555555555","kind":"tracked","beginAt":"2026-08-14T07:30:00.000Z","endAt":"2026-08-14T09:00:00.000Z","durationMinutes":90,"durationHours":1.5,"analysis":{"status":"ok","errorMessage":null,"errorCode":null,"calculatedAt":"2026-08-14T09:05:00.000Z"},"comment":"Implement invoice export","finalized":true,"pricing":{"billable":true,"rate":{"id":"aa000001-0000-4000-8000-000000000001","amount":120,"name":"Standard","source":"task","frozen":false},"amount":180},"billing":{"billed":false,"at":null,"by":null,"invoice":null,"readyForBilling":{"at":"2026-08-31T15:40:00.000Z","by":{"id":"bb000001-0000-4000-8000-000000000001","name":"Robin Example"}}},"user":{"id":"66666666-6666-4666-8666-666666666666","name":"Alex Example"},"project":{"id":"77777777-7777-4777-8777-777777777777","name":"Customer portal","projectNumber":"P-2026-001"},"task":{"id":"33333333-3333-4333-8333-333333333333","name":"Implementation","path":[{"id":"11111111-1111-4111-8111-111111111111","name":"Engineering","bookable":false,"kind":"subArea","order":1},{"id":"22222222-2222-4222-8222-222222222222","name":"Development","bookable":false,"kind":"serviceArea","order":1},{"id":"33333333-3333-4333-8333-333333333333","name":"Implementation","bookable":true,"kind":"activity","order":1}]},"updatedAt":"2026-08-14T09:05:00.000Z","billingRevision":null}],"pagination":{"nextCursor":null,"hasMore":false}}},"User":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Stable identifier — the same value `TimeBooking.user.id` carries, and the `userId` filter takes."},"name":{"type":["string","null"],"description":"Display name, or null if unset. Neither unique nor stable: two colleagues can share one and an administrator can change it, so map people by `id` and treat this as a label to show."},"email":{"type":["string","null"],"description":"Work email address, or null when the person has no real mailbox — TimeFusion generates a placeholder address for members an administrator maintains on their behalf, and it is withheld rather than published as if it were reachable. Unique within the organization while set, so it works as a fallback for matching an employee record the first time; it can be changed afterwards, so keep `id` as the stored key."},"deactivated":{"type":"boolean","description":"Whether the person is deactivated in TimeFusion. A person whose flag was never set reports `false`, which is how the app treats it. Deactivated people are returned, not hidden: their historical bookings are still on invoices and still have to resolve to a name. Filter with `?deactivated=false` for the live roster. A deactivated person keeps their bookings and stays billable; this says nothing about money."},"monthlyCloseExempt":{"type":"boolean","description":"Whether the monthly-close workflow is skipped for this person. When true, their bookings are never finalized by the monthly-close lock — the organization's change lock still applies. This is why one employee's bookings can stay `finalized: false` while a colleague's do not, which is worth knowing before billing with `requireFinalized: true`: that option rejects exactly those bookings, with `not_finalized` in `details[]`. This flag is stored independently of the organization's workflow setting and may retain either value while `organization.monthlyCloseStartMonth` on `GET /me` is null; in that state it is moot. Only read `false` as \"this person participates\" once that start month is set."},"rate":{"anyOf":[{"$ref":"#/components/schemas/HourlyRate"},{"type":"null"}],"description":"The person's own hourly rate: the last link before the organization default in the chain that prices a booking (a rate on the booking itself, then the task's, then the project's, then the customer's, then this one, then the organization default, first one set wins), so it applies to a booking of this person only when none of the levels above it has a rate. Not necessarily the rate any given booking bills. Null when no rate is stored on the person, and when the key may not see money (see `money.visible` on `GET /me`). A rate archived in the app stays attached to the people already using it and is still reported here."},"updatedAt":{"type":"string","format":"date-time","description":"When this person's row was last written, in UTC. Upsert on it rather than comparing whole bodies. Every field published here is a column of this row except the `rate`'s `name` and `amount`. It does **not** advance when this person's `rate` is edited: a rate's `name` and `amount` live on the rate itself, so changing either moves the rate's own `updatedAt` and leaves this person row untouched; `GET /rates` is where that change shows up. Assigning a different rate, or clearing it, does move it: which rate applies is a column on this row. The response's `ETag` still changes, because it covers the body.","example":"2026-08-14T07:30:00.000Z"}},"required":["id","name","email","deactivated","monthlyCloseExempt","rate","updatedAt"],"description":"A person in the organization, as the employee mapping needs them. Every member is returned, including one who has never recorded a booking, so the mapping can be built before the first sync rather than discovered from the bookings that happen to exist. Roles, permissions and departments are deliberately not exposed.","example":{"id":"66666666-6666-4666-8666-666666666666","name":"Alex Example","email":"alex.example@example.com","deactivated":false,"monthlyCloseExempt":false,"rate":{"id":"aa000004-0000-4000-8000-000000000004","amount":95,"name":"Senior"},"updatedAt":"2026-08-14T09:05:00.000Z"}},"UserPage":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/User"},"description":"The records on this page, in cursor order."},"pagination":{"$ref":"#/components/schemas/Pagination"}},"required":["data","pagination"],"description":"One page of people in the organization, oldest first. Pass `pagination.nextCursor` back as `cursor` and stop when `hasMore` is false; there is no total count.","example":{"data":[{"id":"66666666-6666-4666-8666-666666666666","name":"Alex Example","email":"alex.example@example.com","deactivated":false,"monthlyCloseExempt":false,"rate":{"id":"aa000004-0000-4000-8000-000000000004","amount":95,"name":"Senior"},"updatedAt":"2026-08-14T09:05:00.000Z"}],"pagination":{"nextCursor":null,"hasMore":false}}},"UserRef":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Stable user identifier, usable as the `userId` filter."},"name":{"type":["string","null"],"description":"Person's display name, or null if unset."}},"required":["id","name"],"description":"A person in the organization: on a time booking, the employee whose work it records (`user`), or the project admin who marked it ready for billing (`billing.readyForBilling.by`). The `id` is the stable join key against the employee record, and the same value `GET /users` returns. This reference carries no email; the `User` resource does, behind `users:read`.","example":{"id":"66666666-6666-4666-8666-666666666666","name":"Alex Example"}},"ValidationError":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Machine-readable failure reason — one code per reason, and the value a client branches on. The HTTP status is only the class it falls into. This envelope carries `invalid_parameters`, `malformed_json`, `invalid_period`, `period_too_long`, `invalid_cursor`, `invoice_number_required`, `invoice_number_not_allowed`, `invoice_page_too_large`, `idempotency_key_not_allowed`. Published as a string rather than an enum, so a reason named in a later release reaches you as an ordinary response: treat a code you do not know as its status class.","example":"invalid_parameters"},"message":{"type":"string","description":"Human-readable explanation of the failure. May be reworded in any release; branch on `code`."},"details":{"type":"array","items":{"$ref":"#/components/schemas/FieldErrorDetail"},"description":"One entry per offending request field, on `invalid_parameters`. The other `400` codes name a single request-level reason and carry none."}},"required":["code","message"],"description":"The failure, as one machine-readable `code`, a `message` written for a person, and whatever `details[]` this envelope carries."}},"required":["error"],"description":"The error envelope for a request rejected before it ran.","example":{"error":{"code":"invalid_parameters","message":"Invalid request parameters.","details":[{"field":"limit","code":"too_big","message":"`limit` must be at most 200."}]}}}},"parameters":{}},"paths":{"/api/integrations/v1/me":{"get":{"operationId":"getMe","tags":["API-Key & Organization"],"summary":"Introspect the calling API key","description":"Required scope: none. Any valid key may call it, including one granted no scopes. Returns the organization the key belongs to, the key's own metadata, whether monetary amounts will be populated (and if not, which of the two gates closed), whether the organization uses the monthly close and the ready-for-billing mark at all, and the current rate-limit window.","x-required-scopes":[],"x-badges":[{"name":"any key","position":"before"}],"x-scalar-stability":"stable","responses":{"200":{"description":"The calling key and its organization.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"Cache-Control":{"description":"`private` keeps a body scoped to one API key out of shared caches; `no-cache` means revalidate before reuse, not do not store.","required":true,"schema":{"type":"string","example":"private, no-cache"}},"Vary":{"description":"The response depends on the key that asked, so an intermediary that ignores `private` still may not serve it to another one.","required":true,"schema":{"type":"string","example":"Authorization"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyContext"}}}},"401":{"description":"The key is missing, unknown, revoked or expired, or the organization has switched the Integrations API off, and the response deliberately does not say which. Send a valid key as `Authorization: Bearer sk_tf_int_live_…`. If a key that worked before answers `401`, have an administrator check that it was not revoked or expired and that the API is still switched on.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"WWW-Authenticate":{"description":"Bearer authentication challenge returned on every 401.","required":true,"schema":{"type":"string","example":"Bearer"}}},"content":{"application/json":{"example":{"error":{"code":"unauthorized","message":"Missing or invalid API key. Send it as `Authorization: Bearer sk_tf_int_live_…`."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"The per-key budget for this window is spent. Failed-authentication throttling carries no per-key `X-RateLimit-*` headers. Wait for `Retry-After`, a delta in seconds, then retry.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"Retry-After":{"description":"Minimum delay before retrying, in seconds (a delta, not Unix epoch seconds).","required":true,"schema":{"type":"integer","minimum":1,"example":30}}},"content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"An unexpected error, with a `requestId` for support. Quote the `requestId` (or `X-Request-Id`) in a support request.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"internal_error","message":"An unexpected error occurred. Quote the request ID when contacting support.","requestId":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-codeSamples":[{"lang":"typescript","label":"TypeScript (fetch)","source":"// The first call: which organization, which key, whether money arrives\n// populated, and how much of the rate-limit window is left.\nconst response = await fetch(process.env.TF_ORIGIN + \"/api/integrations/v1/me\", {\n  headers: { Authorization: \"Bearer \" + process.env.TF_KEY },\n});\nif (!response.ok) throw new Error(\"HTTP \" + response.status);\nconst me = await response.json();\n\n// Read this at startup. When false, every rate and amount in every response is\n// null, and the reason names which of the two gates closed.\nif (!me.money.visible) {\n  console.warn(\"money unavailable:\", me.money.unavailableReason);\n}\n\n// 120 for reads; writes have their own window of 20/minute.\nconsole.log(me.organization.timeZone, me.rateLimit.remaining, \"of\", me.rateLimit.limit);\n// GET /api/integrations/v1/me is the one read with no ETag: its body changes every call."}]}},"/api/integrations/v1/time-bookings":{"get":{"operationId":"listTimeBookings","tags":["Time bookings"],"summary":"List time bookings in a period","description":"Required scope: `bookings:read`. Returns the organization's time bookings, oldest first, in pages walked with `cursor`. A tracked booking that is still running is left out, even when asked for by `id`; `GET /time-bookings/{id}` returns it. `sort` chooses the ordering and what `from`/`to` measure: the work's own period by default, or last modification with `sort=updatedAt`. `from` and `to` are required **unless** `invoiceNumber` or `id` is given, either of which bounds the result on its own, so \"what is on this invoice?\" and \"what happened to these bookings?\" need no guess at a window containing them.","x-required-scopes":["bookings:read"],"x-badges":[{"name":"bookings:read","position":"before"}],"x-scalar-stability":"stable","parameters":[{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}([T ]\\d{2}:\\d{2}(:\\d{2}(\\.\\d{1,3})?)?(Z|[+-]\\d{2}:\\d{2})?)?$","description":"Inclusive start of the window. **Required unless `invoiceNumber` or `id` is given**, since each bounds the result on its own; either boundary may be supplied alone as an open-ended bound. When both are given, the maximum span still applies. With the default `sort=beginAt` it selects by period — tracked bookings by endAt, plain bookings by beginAt. With `sort=updatedAt` it selects by last modification instead. Running tracked bookings are excluded either way. A date without a time means 00:00:00.000 UTC on that day. Timestamps without an offset are interpreted as UTC; explicit offsets are respected.","example":"2026-08-01"},"required":false,"description":"Inclusive start of the window. **Required unless `invoiceNumber` or `id` is given**, since each bounds the result on its own; either boundary may be supplied alone as an open-ended bound. When both are given, the maximum span still applies. With the default `sort=beginAt` it selects by period — tracked bookings by endAt, plain bookings by beginAt. With `sort=updatedAt` it selects by last modification instead. Running tracked bookings are excluded either way. A date without a time means 00:00:00.000 UTC on that day. Timestamps without an offset are interpreted as UTC; explicit offsets are respected.","name":"from","in":"query"},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}([T ]\\d{2}:\\d{2}(:\\d{2}(\\.\\d{1,3})?)?(Z|[+-]\\d{2}:\\d{2})?)?$","description":"Inclusive end of the window. **Required unless `invoiceNumber` or `id` is given**, since each bounds the result on its own; either boundary may be supplied alone as an open-ended bound. When both are given, the maximum span still applies. With the default `sort=beginAt` it selects by period — tracked bookings by endAt, plain bookings by beginAt. With `sort=updatedAt` it selects by last modification instead. Running tracked bookings are excluded either way. A date without a time means 23:59:59.999 UTC on that day. Timestamps without an offset are interpreted as UTC; explicit offsets are respected.","example":"2026-08-31"},"required":false,"description":"Inclusive end of the window. **Required unless `invoiceNumber` or `id` is given**, since each bounds the result on its own; either boundary may be supplied alone as an open-ended bound. When both are given, the maximum span still applies. With the default `sort=beginAt` it selects by period — tracked bookings by endAt, plain bookings by beginAt. With `sort=updatedAt` it selects by last modification instead. Running tracked bookings are excluded either way. A date without a time means 23:59:59.999 UTC on that day. Timestamps without an offset are interpreted as UTC; explicit offsets are respected.","name":"to","in":"query"},{"schema":{"type":"array","items":{"type":"string","format":"uuid"},"minItems":1,"maxItems":100,"description":"Only the bookings named here. Like `invoiceNumber`, this bounds the result set on its own, so **both period boundaries become optional** — it is how the set a billing write echoed is read back without guessing a window wide enough to contain it. An unknown id contributes no rows rather than an error, exactly as one alone yields an empty page. Repeat the parameter for several values — `?id=a&id=b` — and a booking matches any of them; at most 100 per request. Repeated values within one parameter are ORed, different parameters are ANDed, and duplicates are ignored. Bracketed keys (`id[]`, `id[0]`) are rejected with `400`.","example":["11111111-1111-4111-8111-111111111111","22222222-2222-4222-8222-222222222222"]},"required":false,"description":"Only the bookings named here. Like `invoiceNumber`, this bounds the result set on its own, so **both period boundaries become optional** — it is how the set a billing write echoed is read back without guessing a window wide enough to contain it. An unknown id contributes no rows rather than an error, exactly as one alone yields an empty page. Repeat the parameter for several values — `?id=a&id=b` — and a booking matches any of them; at most 100 per request. Repeated values within one parameter are ORed, different parameters are ANDed, and duplicates are ignored. Bracketed keys (`id[]`, `id[0]`) are rejected with `400`.","name":"id","in":"query","style":"form","explode":true},{"schema":{"type":"array","items":{"type":"string","format":"uuid"},"minItems":1,"maxItems":50,"description":"Only bookings on this project. Repeat the parameter for several values — `?projectId=a&projectId=b` — and a booking matches any of them; at most 50 per request. Repeated values within one parameter are ORed, different parameters are ANDed, and duplicates are ignored. Bracketed keys (`projectId[]`, `projectId[0]`) are rejected with `400`.","example":["11111111-1111-4111-8111-111111111111","22222222-2222-4222-8222-222222222222"]},"required":false,"description":"Only bookings on this project. Repeat the parameter for several values — `?projectId=a&projectId=b` — and a booking matches any of them; at most 50 per request. Repeated values within one parameter are ORed, different parameters are ANDed, and duplicates are ignored. Bracketed keys (`projectId[]`, `projectId[0]`) are rejected with `400`.","name":"projectId","in":"query","style":"form","explode":true},{"schema":{"type":"array","items":{"type":"string","format":"uuid"},"minItems":1,"maxItems":50,"description":"Only bookings on this task. Repeat the parameter for several values — `?taskId=a&taskId=b` — and a booking matches any of them; at most 50 per request. Repeated values within one parameter are ORed, different parameters are ANDed, and duplicates are ignored. Bracketed keys (`taskId[]`, `taskId[0]`) are rejected with `400`.","example":["11111111-1111-4111-8111-111111111111","22222222-2222-4222-8222-222222222222"]},"required":false,"description":"Only bookings on this task. Repeat the parameter for several values — `?taskId=a&taskId=b` — and a booking matches any of them; at most 50 per request. Repeated values within one parameter are ORed, different parameters are ANDed, and duplicates are ignored. Bracketed keys (`taskId[]`, `taskId[0]`) are rejected with `400`.","name":"taskId","in":"query","style":"form","explode":true},{"schema":{"type":"array","items":{"type":"string","format":"uuid"},"minItems":1,"maxItems":50,"description":"Only bookings on projects of this customer. Repeat the parameter for several values — `?customerId=a&customerId=b` — and a booking matches any of them; at most 50 per request. Repeated values within one parameter are ORed, different parameters are ANDed, and duplicates are ignored. Bracketed keys (`customerId[]`, `customerId[0]`) are rejected with `400`.","example":["11111111-1111-4111-8111-111111111111","22222222-2222-4222-8222-222222222222"]},"required":false,"description":"Only bookings on projects of this customer. Repeat the parameter for several values — `?customerId=a&customerId=b` — and a booking matches any of them; at most 50 per request. Repeated values within one parameter are ORed, different parameters are ANDed, and duplicates are ignored. Bracketed keys (`customerId[]`, `customerId[0]`) are rejected with `400`.","name":"customerId","in":"query","style":"form","explode":true},{"schema":{"type":"array","items":{"type":"string","format":"uuid"},"minItems":1,"maxItems":50,"description":"Only bookings of this person. Repeat the parameter for several values — `?userId=a&userId=b` — and a booking matches any of them; at most 50 per request. Repeated values within one parameter are ORed, different parameters are ANDed, and duplicates are ignored. Bracketed keys (`userId[]`, `userId[0]`) are rejected with `400`.","example":["11111111-1111-4111-8111-111111111111","22222222-2222-4222-8222-222222222222"]},"required":false,"description":"Only bookings of this person. Repeat the parameter for several values — `?userId=a&userId=b` — and a booking matches any of them; at most 50 per request. Repeated values within one parameter are ORed, different parameters are ANDed, and duplicates are ignored. Bracketed keys (`userId[]`, `userId[0]`) are rejected with `400`.","name":"userId","in":"query","style":"form","explode":true},{"schema":{"type":"string","enum":["true","false"],"description":"Filter by billable flag. `false` includes bookings whose flag has never been set as well as explicitly non-billable ones."},"required":false,"description":"Filter by billable flag. `false` includes bookings whose flag has never been set as well as explicitly non-billable ones.","name":"billable","in":"query"},{"schema":{"type":"string","enum":["true","false"],"description":"Filter by billing status. `true` returns bookings marked as billed; `false` returns every other booking, including those the billing flow has never touched."},"required":false,"description":"Filter by billing status. `true` returns bookings marked as billed; `false` returns every other booking, including those the billing flow has never touched.","name":"billed","in":"query"},{"schema":{"type":"string","enum":["true","false"],"description":"Filter by period lock — the organization's change lock or the booking owner's monthly-close lock. `true` returns only bookings whose recorded time is finalized, which is the selection an invoicing run wants alongside `billable=true&billed=false`. Evaluated exactly as the `finalized` field on each booking, so the two can never disagree. This is current lock state, not a guarantee: reopening a period can move a booking out of the filtered set. The ready-for-billing mark is a separate lock with its own filter, `readyForBilling`."},"required":false,"description":"Filter by period lock — the organization's change lock or the booking owner's monthly-close lock. `true` returns only bookings whose recorded time is finalized, which is the selection an invoicing run wants alongside `billable=true&billed=false`. Evaluated exactly as the `finalized` field on each booking, so the two can never disagree. This is current lock state, not a guarantee: reopening a period can move a booking out of the filtered set. The ready-for-billing mark is a separate lock with its own filter, `readyForBilling`.","name":"finalized","in":"query"},{"schema":{"type":"string","enum":["true","false"],"description":"Filter by the ready-for-billing mark. `true` returns only bookings a project admin released to accounting, the selection for an organization whose process requires that release; `false` returns every other booking. Evaluated exactly as `billing.readyForBilling` on each booking: `true` means it is not null. Combine with `billed=false`, since the mark stays after billing."},"required":false,"description":"Filter by the ready-for-billing mark. `true` returns only bookings a project admin released to accounting, the selection for an organization whose process requires that release; `false` returns every other booking. Evaluated exactly as `billing.readyForBilling` on each booking: `true` means it is not null. Combine with `billed=false`, since the mark stays after billing.","name":"readyForBilling","in":"query"},{"schema":{"type":"array","items":{"type":"string","enum":["notApplicable","pending","ok","error"]},"minItems":1,"maxItems":4,"description":"Only bookings whose stored analysis is in this state. Repeat the parameter for several values — `?analysisStatus=ok&analysisStatus=notApplicable` — and a booking matches any of them; at most 4 per request. Repeated values within one parameter are ORed, different parameters are ANDed, and duplicates are ignored. Bracketed keys (`analysisStatus[]`, `analysisStatus[0]`) are rejected with `400`. `ok` with `notApplicable` returns successfully analyzed tracked bookings alongside plain-hour entries. Evaluated exactly as the `analysis.status` field on each booking, so the filtered list and the field on each row can never disagree. Omit to include every status; running bookings remain excluded from lists.","example":["ok","notApplicable"]},"required":false,"description":"Only bookings whose stored analysis is in this state. Repeat the parameter for several values — `?analysisStatus=ok&analysisStatus=notApplicable` — and a booking matches any of them; at most 4 per request. Repeated values within one parameter are ORed, different parameters are ANDed, and duplicates are ignored. Bracketed keys (`analysisStatus[]`, `analysisStatus[0]`) are rejected with `400`. `ok` with `notApplicable` returns successfully analyzed tracked bookings alongside plain-hour entries. Evaluated exactly as the `analysis.status` field on each booking, so the filtered list and the field on each row can never disagree. Omit to include every status; running bookings remain excluded from lists.","name":"analysisStatus","in":"query","style":"form","explode":true},{"schema":{"type":"array","items":{"type":"string","minLength":1},"minItems":1,"maxItems":20,"description":"Exact invoice reference match. With this filter, either or both period boundaries may be omitted to enumerate all linked bookings, including work from earlier years. The same reference filters GET /invoices via its number parameter. URL-encode the value — and send one occurrence per reference rather than joining them, since a reference may itself contain a comma. Repeat the parameter for several values — `?invoiceNumber=RE-2026-0815&invoiceNumber=RE-2026-0816` — and a booking matches any of them; at most 20 per request. Repeated values within one parameter are ORed, different parameters are ANDed, and duplicates are ignored. Bracketed keys (`invoiceNumber[]`, `invoiceNumber[0]`) are rejected with `400`.","example":["RE-2026-0815","RE-2026-0816"]},"required":false,"description":"Exact invoice reference match. With this filter, either or both period boundaries may be omitted to enumerate all linked bookings, including work from earlier years. The same reference filters GET /invoices via its number parameter. URL-encode the value — and send one occurrence per reference rather than joining them, since a reference may itself contain a comma. Repeat the parameter for several values — `?invoiceNumber=RE-2026-0815&invoiceNumber=RE-2026-0816` — and a booking matches any of them; at most 20 per request. Repeated values within one parameter are ORed, different parameters are ANDed, and duplicates are ignored. Bracketed keys (`invoiceNumber[]`, `invoiceNumber[0]`) are rejected with `400`.","name":"invoiceNumber","in":"query","style":"form","explode":true},{"schema":{"type":"string","enum":["beginAt","updatedAt"],"default":"beginAt","description":"Which order to walk, and which timestamp `from`/`to` bound. `beginAt` (default) walks the work chronologically and windows on the booking period. `updatedAt` walks by last modification and windows on that instead — the ordering a mirror polls to find late edits to old bookings. Cursors are not interchangeable between the two.","example":"beginAt"},"required":false,"description":"Which order to walk, and which timestamp `from`/`to` bound. `beginAt` (default) walks the work chronologically and windows on the booking period. `updatedAt` walks by last modification and windows on that instead — the ordering a mirror polls to find late edits to old bookings. Cursors are not interchangeable between the two.","name":"sort","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":200,"default":50,"description":"Page size, 1–200. Values above 200 are rejected, not clamped.","example":50},"required":false,"description":"Page size, 1–200. Values above 200 are rejected, not clamped.","name":"limit","in":"query"},{"schema":{"type":"string","description":"Opaque cursor from a previous page's `pagination.nextCursor` **on this endpoint**. Cursors are not interchangeable between endpoints; one issued elsewhere is rejected as `invalid_cursor`. Omit for the first page."},"required":false,"description":"Opaque cursor from a previous page's `pagination.nextCursor` **on this endpoint**. Cursors are not interchangeable between endpoints; one issued elsewhere is rejected as `invalid_cursor`. Omit for the first page.","name":"cursor","in":"query"}],"responses":{"200":{"description":"One page of time bookings.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"ETag":{"description":"Strong validator over this exact body. Send it back as `If-None-Match` on the next poll and an unchanged page answers `304` with no body. It covers the whole request — filters, cursor, and whether this key may see money — so tags from two different keys or two different queries never match.","required":true,"schema":{"type":"string","example":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\""}},"Cache-Control":{"description":"`no-cache` means revalidate before reuse, not do not store — it is what makes a conditional request happen. `private` keeps a body scoped to one API key out of shared caches.","required":true,"schema":{"type":"string","example":"private, no-cache"}},"Vary":{"description":"The response depends on the key that asked: two keys for the same organization can see different amounts on the same URL.","required":true,"schema":{"type":"string","example":"Authorization"}}},"content":{"application/json":{"examples":{"Ready to invoice":{"summary":"Ready to invoice","description":"`?finalized=true&billable=true&billed=false&analysisStatus=ok&analysisStatus=notApplicable` — completed work, chargeable, not yet billed, and clean enough for the billing write to accept. Save each `billingRevision` if you intend to bill against reviewed state. `hasMore` is true, so pass `nextCursor` back as `cursor` and keep walking.","value":{"data":[{"id":"55555555-5555-4555-8555-555555555555","kind":"tracked","beginAt":"2026-08-14T07:30:00.000Z","endAt":"2026-08-14T09:00:00.000Z","durationMinutes":90,"durationHours":1.5,"analysis":{"status":"ok","errorMessage":null,"errorCode":null,"calculatedAt":"2026-08-14T09:05:00.000Z"},"comment":"Implement invoice export","finalized":true,"pricing":{"billable":true,"rate":{"id":"aa000001-0000-4000-8000-000000000001","amount":120,"name":"Standard","source":"task","frozen":false},"amount":180},"billing":{"billed":false,"at":null,"by":null,"invoice":null,"readyForBilling":{"at":"2026-08-31T15:40:00.000Z","by":{"id":"bb000001-0000-4000-8000-000000000001","name":"Robin Example"}}},"user":{"id":"66666666-6666-4666-8666-666666666666","name":"Alex Example"},"project":{"id":"77777777-7777-4777-8777-777777777777","name":"Customer portal","projectNumber":"P-2026-001"},"task":{"id":"33333333-3333-4333-8333-333333333333","name":"Implementation","path":[{"id":"11111111-1111-4111-8111-111111111111","name":"Engineering","bookable":false,"kind":"subArea","order":1},{"id":"22222222-2222-4222-8222-222222222222","name":"Development","bookable":false,"kind":"serviceArea","order":1},{"id":"33333333-3333-4333-8333-333333333333","name":"Implementation","bookable":true,"kind":"activity","order":1}]},"updatedAt":"2026-08-14T09:05:00.000Z","billingRevision":"br1.2026a.ZQRRNAZN1NkFZl7Or0FT_qT51p0q4iJVoLfO9HAfSIY"}],"pagination":{"nextCursor":"eyJ2IjoxLCJzIjoidGltZS1ib29raW5ncyIsImsiOlsiMjAyNi0wOC0xNFQwNzozMDowMC4wMDBaIiwiNTU1NTU1NTUtNTU1NS00NTU1LTg1NTUtNTU1NTU1NTU1NTU1Il19","hasMore":true}}},"After billing":{"summary":"After billing","description":"The same booking once it has been marked. `billing.billed`, `at`, `by` and `invoice` are set together, and `billingRevision` is a different token — `billed` is one of the inputs it covers, so a precondition holding the old value would now refuse. The walk has ended: `hasMore` is false and `nextCursor` is null.","value":{"data":[{"id":"55555555-5555-4555-8555-555555555555","kind":"tracked","beginAt":"2026-08-14T07:30:00.000Z","endAt":"2026-08-14T09:00:00.000Z","durationMinutes":90,"durationHours":1.5,"analysis":{"status":"ok","errorMessage":null,"errorCode":null,"calculatedAt":"2026-08-14T09:05:00.000Z"},"comment":"Implement invoice export","finalized":true,"pricing":{"billable":true,"rate":{"id":"aa000001-0000-4000-8000-000000000001","amount":120,"name":"Standard","source":"task","frozen":false},"amount":180},"billing":{"billed":true,"at":"2026-09-01T08:12:00.000Z","by":{"type":"api_key","id":"44444444-4444-4444-8444-444444444444","name":"Invoice sync"},"invoice":{"id":"99999999-9999-4999-8999-999999999999","number":"2026-0042"},"readyForBilling":{"at":"2026-08-31T15:40:00.000Z","by":{"id":"bb000001-0000-4000-8000-000000000001","name":"Robin Example"}}},"user":{"id":"66666666-6666-4666-8666-666666666666","name":"Alex Example"},"project":{"id":"77777777-7777-4777-8777-777777777777","name":"Customer portal","projectNumber":"P-2026-001"},"task":{"id":"33333333-3333-4333-8333-333333333333","name":"Implementation","path":[{"id":"11111111-1111-4111-8111-111111111111","name":"Engineering","bookable":false,"kind":"subArea","order":1},{"id":"22222222-2222-4222-8222-222222222222","name":"Development","bookable":false,"kind":"serviceArea","order":1},{"id":"33333333-3333-4333-8333-333333333333","name":"Implementation","bookable":true,"kind":"activity","order":1}]},"updatedAt":"2026-08-14T09:05:00.000Z","billingRevision":"br1.2026a.tjumQI7gp7SPGzd_ZCPWjZyNlaSoeFAC58DoaT6eWyw"}],"pagination":{"nextCursor":null,"hasMore":false}}}},"schema":{"$ref":"#/components/schemas/TimeBookingPage"}}}},"304":{"description":"The representation is unchanged since the `ETag` in `If-None-Match`. No body. This still counts against the rate limit — the server ran the query and built the response; only the transfer was skipped.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"ETag":{"description":"Strong validator over this exact body. Send it back as `If-None-Match` on the next poll and an unchanged page answers `304` with no body. It covers the whole request — filters, cursor, and whether this key may see money — so tags from two different keys or two different queries never match.","required":true,"schema":{"type":"string","example":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\""}}}},"400":{"description":"The request was rejected before it ran. `error.code` says why:\n\n- `invalid_parameters` — a query, path or body value does not match the schema. Fix the fields `details[]` names.\n- `invalid_period` — `from` or `to` is date-shaped but not a real date (`2026-13-01`, `2026-02-31`), or `to` is earlier than `from`. A boundary that is not date-shaped at all fails the schema as `invalid_parameters` instead. Send a real calendar date, with `to` no earlier than `from`.\n- `period_too_long` — the window is wider than the maximum span. Split the sync into narrower windows.\n- `invalid_cursor` — the `cursor` was not issued by this API. Discard the cursor and restart the walk from the first page.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"invalid_parameters","message":"Invalid request parameters.","details":[{"field":"limit","code":"too_big","message":"`limit` must be at most 200."}]}},"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"401":{"description":"The key is missing, unknown, revoked or expired, or the organization has switched the Integrations API off, and the response deliberately does not say which. Send a valid key as `Authorization: Bearer sk_tf_int_live_…`. If a key that worked before answers `401`, have an administrator check that it was not revoked or expired and that the API is still switched on.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"WWW-Authenticate":{"description":"Bearer authentication challenge returned on every 401.","required":true,"schema":{"type":"string","example":"Bearer"}}},"content":{"application/json":{"example":{"error":{"code":"unauthorized","message":"Missing or invalid API key. Send it as `Authorization: Bearer sk_tf_int_live_…`."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key is missing the `bookings:read` scope this endpoint requires. Scopes are fixed at key creation, so issue a new key carrying the scope named in the message.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"forbidden_scope","message":"This API key is missing the required scope `bookings:read`. Scopes are fixed at key creation — issue a new key to change them."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"The per-key budget for this window is spent. Failed-authentication throttling carries no per-key `X-RateLimit-*` headers. Wait for `Retry-After`, a delta in seconds, then retry.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"Retry-After":{"description":"Minimum delay before retrying, in seconds (a delta, not Unix epoch seconds).","required":true,"schema":{"type":"integer","minimum":1,"example":30}}},"content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"An unexpected error, with a `requestId` for support. Quote the `requestId` (or `X-Request-Id`) in a support request.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"internal_error","message":"An unexpected error occurred. Quote the request ID when contacting support.","requestId":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-codeSamples":[{"lang":"typescript","label":"TypeScript (fetch)","source":"// The cursor walk, Retry-After and If-None-Match are three sections of the\n// guide and one piece of code. Read it in this order:\n//\n//   TimeFusionClient.request - retries a 429 after Retry-After, and reuses the\n//                              cached body on a 304, which carries none of its own.\n//   TimeFusionClient.pages   - follows pagination.nextCursor to the end of the walk.\n//   collectById              - collects a walk by booking id, because a row\n//                              edited mid-walk can arrive twice.\n//   syncWindow               - walks a window and hands back the checkpoint to\n//                              save, only after the whole walk succeeded.\n//\n// The whole client, with typed methods for every operation, is client.ts in\n// the starter kit; sync.ts adds durable file storage.\n\n/**\n * The API version this copy was written against. Every response names the live\n * one in `X-Api-Version`, and the client warns on any difference. Update it\n * only after reviewing the changes, or replace this file with a new copy.\n */\nexport const EXPECTED_API_VERSION = \"1.2.0\";\n// ---------------------------------------------------------------------------\n// Resources, as the API returns them\n// ---------------------------------------------------------------------------\n\n/**\n * A value from a published vocabulary.\n *\n * The API names a field's current values in its description and may add one in\n * any release (see _Versioning_), so these fields are typed to accept a value\n * this file has never heard of. Branch on the ones you know and keep a default\n * case; the literals are here so an editor still completes them.\n */\nexport type Open<Known extends string> = Known | (string & {});\n\nexport type InvoiceRef = { id: string; number: string };\n\n/** A task's ancestors from the root down, ending with the task itself. */\nexport type TaskPath = {\n  id: string;\n  name: string;\n  bookable: boolean;\n  kind: string | null;\n  order: number;\n}[];\n\nexport type AnalysisStatus = \"ok\" | \"notApplicable\" | \"pending\" | \"error\";\n\nexport type TimeBooking = {\n  id: string;\n  kind: Open<\"tracked\" | \"plain\">;\n  beginAt: string;\n  /** Null while a tracked booking runs, and always on a `plain` entry. */\n  endAt: string | null;\n  durationMinutes: number | null;\n  durationHours: number | null;\n  analysis: {\n    status: Open<AnalysisStatus>;\n    errorMessage: string | null;\n    errorCode: Open<\n      | \"outsideOfBoundsPartial\"\n      | \"outsideOfBounds\"\n      | \"overlapWithOther\"\n      | \"missingEnd\"\n      | \"endBeforeBegin\"\n    > | null;\n    calculatedAt: string | null;\n  };\n  comment: string | null;\n  finalized: boolean;\n  /**\n   * `rate` and `amount` are null when the key may not see money (check\n   * `getMe()`), and on work that is not billable, running or unpriced.\n   */\n  pricing: {\n    billable: boolean;\n    rate: {\n      /** Null on a frozen rate: the snapshot records no id. */\n      id: string | null;\n      amount: number;\n      name: string | null;\n      source: Open<\n        \"booking\" | \"task\" | \"project\" | \"customer\" | \"user\" | \"org\"\n      > | null;\n      frozen: boolean;\n    } | null;\n    amount: number | null;\n  };\n  billing: {\n    billed: boolean;\n    at: string | null;\n    by:\n      | { type: \"user\"; id: string; name: string | null }\n      | { type: \"api_key\"; id: string; name: string }\n      | null;\n    invoice: InvoiceRef | null;\n    /**\n     * The project admin's release to accounting, or null. Stays after billing.\n     * `by` is null once the marking person's account is deleted.\n     */\n    readyForBilling: {\n      at: string;\n      by: { id: string; name: string | null } | null;\n    } | null;\n  };\n  user: { id: string; name: string | null };\n  project: { id: string; name: string; projectNumber: string | null } | null;\n  task: { id: string; name: string; path: TaskPath };\n  updatedAt: string;\n  /** Null when the key may not see money or the server signs no revisions. */\n  billingRevision: string | null;\n};\n\nexport type Scope =\n  | \"bookings:read\"\n  | \"masterdata:read\"\n  | \"customers:write\"\n  | \"projects:write\"\n  | \"users:read\"\n  | \"billing:read\"\n  | \"billing:write\"\n  | \"money:read\";\n\nexport type ApiKeyContext = {\n  organization: {\n    id: string;\n    name: string;\n    currency: string;\n    timeZone: string;\n    /**\n     * `YYYY-MM`, or null when the organization runs no monthly close at all.\n     * With null, a person's independently stored `monthlyCloseExempt` value is\n     * moot and no booking is report-locked — `finalized` comes from the change\n     * lock alone.\n     */\n    monthlyCloseStartMonth: string | null;\n    /** Whether project admins can mark bookings ready for billing here. */\n    readyForBillingEnabled: boolean;\n  };\n  key: {\n    id: string;\n    name: string;\n    prefix: string;\n    scopes: Open<Scope>[];\n    createdAt: string;\n    /** Set once the key was replaced: it stops working at this instant. */\n    expiresAt: string | null;\n  };\n  money: {\n    visible: boolean;\n    unavailableReason: Open<\"missing_scope\" | \"accounting_disabled\"> | null;\n  };\n  /** Null when the server runs without a rate limiter. */\n  rateLimit: { limit: number; remaining: number; resetAt: string } | null;\n};\n\n/**\n * Who created or last changed a customer or project. Your own writes carry\n * `type: \"api_key\"` and `getMe()`'s `key.id`.\n */\nexport type RecordActor =\n  | { type: \"user\"; id: string; name: string | null }\n  | { type: \"api_key\"; id: string; name: string };\n\nexport type Project = {\n  id: string;\n  /** Your system's id for it, set through this API; null when unlinked. */\n  externalId: string | null;\n  name: string;\n  projectNumber: string | null;\n  color: string;\n  /** False when `color` is derived from the id rather than set. */\n  colorSet: boolean;\n  deactivated: boolean;\n  customer: { id: string; name: string } | null;\n  rate: { id: string; amount: number; name: string } | null;\n  /** The budget in hours; null when none is set. */\n  plannedHours: number | null;\n  createdBy: RecordActor | null;\n  updatedBy: RecordActor | null;\n  updatedAt: string;\n};\n\n/** `getProjectHours`: summed live, every sum in decimal hours. */\nexport type ProjectHours = {\n  projectId: string;\n  plannedHours: number | null;\n  spentHours: number;\n  /** Splits into open, `readyForBillingHours` and `billedHours`. */\n  billableHours: number;\n  readyForBillingHours: number;\n  billedHours: number;\n  /** Timers with no end yet; they count 0 in every sum until they end. */\n  runningBookings: number;\n};\n\nexport type Task = {\n  id: string;\n  name: string;\n  project: { id: string; name: string; projectNumber: string | null } | null;\n  kind: string | null;\n  bookable: boolean;\n  parentTaskId: string | null;\n  path: TaskPath;\n  billableByDefault: boolean;\n  rate: { id: string; amount: number; name: string } | null;\n  updatedAt: string;\n};\n\nexport type Customer = {\n  id: string;\n  externalId: string | null;\n  name: string;\n  archived: boolean;\n  rate: { id: string; amount: number; name: string } | null;\n  createdBy: RecordActor | null;\n  updatedBy: RecordActor | null;\n  updatedAt: string;\n};\n\nexport type RateCatalogEntry = {\n  id: string;\n  externalId: string | null;\n  name: string;\n  /** Null when the key may not see money; the rest of the entry stays. */\n  amount: number | null;\n  description: string | null;\n  archived: boolean;\n  isOrganizationDefault: boolean;\n  createdBy: RecordActor | null;\n  updatedBy: RecordActor | null;\n  updatedAt: string;\n};\n\nexport type User = {\n  id: string;\n  name: string | null;\n  email: string | null;\n  deactivated: boolean;\n  monthlyCloseExempt: boolean;\n  rate: { id: string; amount: number; name: string } | null;\n  updatedAt: string;\n};\n\nexport type Invoice = {\n  invoice: InvoiceRef;\n  createdAt: string;\n  timeBookingCount: number;\n  timeBookingAmount: number | null;\n  unpricedTimeBookingCount: number | null;\n  timeBookingAmountComplete: boolean | null;\n  expenseCount: number;\n  expenseAmount: number | null;\n};\n\nexport type BillingRequest = {\n  /** At most `LIMITS.billingBatch` per request. */\n  bookingIds: string[];\n  billed: boolean;\n  invoiceNumber?: string;\n  expectedBillingRevisions?: Record<string, string>;\n  requireFinalized?: boolean;\n  requireReadyForBilling?: boolean;\n  dryRun?: boolean;\n};\n\nexport type BillingResult = {\n  invoice: InvoiceRef | null;\n  changed: number;\n  unchanged: number;\n  bookingIds: string[];\n  dryRun: boolean;\n};\n\n/**\n * One customer for `upsertCustomers`, keyed by your own id. Merge semantics: an\n * omitted field stays as stored, `null` clears a nullable one.\n */\nexport type CustomerBatchItem = {\n  externalId: string;\n  /** The `ETag` you last read: refuses the item if the customer changed since. */\n  ifMatch?: string;\n  name: string;\n  archived?: boolean;\n  /** Needs `money:read` and the accounting feature, even to send `null`. */\n  rateId?: string | null;\n  /**\n   * The rate by your id, as `upsertRates` linked it; never with `rateId`.\n   * `null` clears it.\n   */\n  rateExternalId?: string | null;\n};\n\n/**\n * One hourly rate for `upsertRates`; see `CustomerBatchItem`. Every rate write\n * needs `money:read` and the accounting feature.\n */\nexport type RateBatchItem = {\n  externalId: string;\n  ifMatch?: string;\n  name: string;\n  /** At most two decimals. Re-prices bookings not yet billed. */\n  amount: number;\n  description?: string | null;\n  archived?: boolean;\n};\n\n/** One project for `upsertProjects`; see `CustomerBatchItem`. */\nexport type ProjectBatchItem = {\n  externalId: string;\n  ifMatch?: string;\n  name: string;\n  projectNumber?: string | null;\n  /** `#RRGGBB`, or `null` for the color derived from the project's id. */\n  color?: string | null;\n  /**\n   * The customer by TimeFusion id, or by your id below; never both. `null`\n   * removes it.\n   */\n  customerId?: string | null;\n  customerExternalId?: string | null;\n  rateId?: string | null;\n  rateExternalId?: string | null;\n  /** The budget in hours, two decimals at most; `null` removes it. */\n  plannedHours?: number | null;\n  deactivated?: boolean;\n  /** Copied members and tasks; applied only when the item creates. */\n  templateProjectId?: string;\n};\n\nexport type MasterDataBatchRequest<Item> = {\n  /** At most `LIMITS.masterDataBatch` per request, each `externalId` once. */\n  items: Item[];\n  dryRun?: boolean;\n  /** Link an unlinked record by name (customer, rate) or number (project). */\n  adoptExisting?: boolean;\n};\n\nexport type BatchOutcome = Open<\"created\" | \"updated\" | \"unchanged\">;\n\nexport type MasterDataBatchResult<T> = {\n  dryRun: boolean;\n  created: number;\n  updated: number;\n  unchanged: number;\n  /** In request order. A dry run's carry no `record` and no `etag`. */\n  items: {\n    externalId: string;\n    outcome: BatchOutcome;\n    /** Null for a dry run's create: that record never existed. */\n    id: string | null;\n    record: T | null;\n    etag: string | null;\n  }[];\n};\n\nexport type Page<T> = {\n  data: T[];\n  pagination: { hasMore: boolean; nextCursor: string | null };\n};\n\n// ---------------------------------------------------------------------------\n// Filters. Every list pages with `limit` (at most 200; this client asks for\n// 200 unless you pass one) and a cursor the client follows for you.\n// ---------------------------------------------------------------------------\n\n/** Several values match any of them, sent as the parameter repeated. */\ntype Many<T> = T | readonly T[];\n\n/**\n * The two orderings every master-data list accepts. `createdAt` is the default;\n * `updatedAt` is the walk a mirror polls to find which rows changed, and the\n * only one `from`/`to` may be sent with.\n */\nexport type MasterDataSort = \"createdAt\" | \"updatedAt\";\n\nexport type TimeBookingQuery = {\n  /**\n   * Both boundaries are required unless `id` or `invoiceNumber` bounds the\n   * list. Send an explicit offset: a bare date means UTC.\n   */\n  from?: string;\n  to?: string;\n  /** `updatedAt` makes `from` and `to` select by last modification instead. */\n  sort?: \"beginAt\" | \"updatedAt\";\n  id?: Many<string>;\n  projectId?: Many<string>;\n  taskId?: Many<string>;\n  customerId?: Many<string>;\n  userId?: Many<string>;\n  billable?: boolean;\n  billed?: boolean;\n  finalized?: boolean;\n  readyForBilling?: boolean;\n  analysisStatus?: Many<AnalysisStatus>;\n  invoiceNumber?: Many<string>;\n  limit?: number;\n};\nexport type ProjectQuery = {\n  id?: Many<string>;\n  externalId?: Many<string>;\n  customerId?: Many<string>;\n  deactivated?: boolean;\n  /**\n   * `updatedAt` walks by last modification instead of creation, and makes\n   * `from`/`to` select by it. `from`/`to` are only accepted with it.\n   */\n  sort?: MasterDataSort;\n  from?: string;\n  to?: string;\n  limit?: number;\n};\nexport type TaskQuery = {\n  id?: Many<string>;\n  projectId?: Many<string>;\n  kind?: Many<string>;\n  sort?: MasterDataSort;\n  from?: string;\n  to?: string;\n  limit?: number;\n};\nexport type CustomerQuery = {\n  id?: Many<string>;\n  externalId?: Many<string>;\n  archived?: boolean;\n  sort?: MasterDataSort;\n  from?: string;\n  to?: string;\n  limit?: number;\n};\nexport type RateCatalogQuery = {\n  id?: Many<string>;\n  externalId?: Many<string>;\n  archived?: boolean;\n  sort?: MasterDataSort;\n  from?: string;\n  to?: string;\n  limit?: number;\n};\nexport type UserQuery = { deactivated?: boolean; limit?: number };\nexport type InvoiceQuery = {\n  number?: Many<string>;\n  /** A window on `createdAt`; either boundary may be left out. */\n  from?: string;\n  to?: string;\n  limit?: number;\n};\n\n/**\n * V1's limits: the largest page, the most ids per billing write, the widest\n * window.\n */\nexport const LIMITS = {\n  pageSize: 200,\n  billingBatch: 500,\n  masterDataBatch: 100,\n  windowMs: 366 * 24 * 60 * 60 * 1000,\n} as const;\n\n/**\n * How long one attempt may take. A write that times out may still commit on the\n * server, so its deadline sits well past the longest a full billing batch can\n * legitimately run, and only a hung connection reaches it.\n */\nexport const TIMEOUT_MS = { read: 15_000, write: 90_000 } as const;\n\nconst ATTEMPTS = 4;\nconst backoff = (attempt: number) => 250 * 2 ** attempt;\n\ntype QueryValue = string | number | boolean;\n\n/**\n * Query parameters for any list. An array sends the parameter once per value\n * (`?projectId=a&projectId=b`), never joined: an invoice number may itself\n * contain a comma. `undefined` leaves the parameter out.\n */\nexport type Query = {\n  [name: string]: QueryValue | readonly QueryValue[] | undefined;\n};\n\nconst toParams = (query: Query): URLSearchParams => {\n  const params = new URLSearchParams();\n  for (const [name, value] of Object.entries(query)) {\n    const values =\n      value === undefined ? [] : typeof value === \"object\" ? value : [value];\n    for (const one of values) params.append(name, String(one));\n  }\n  return params;\n};\n\n/** Every `error.code` the API sends, then the four this client raises itself. */\nexport type ApiErrorCode =\n  | \"invalid_parameters\"\n  | \"malformed_json\"\n  | \"invalid_period\"\n  | \"period_too_long\"\n  | \"invalid_cursor\"\n  | \"invoice_number_required\"\n  | \"invoice_number_not_allowed\"\n  | \"invoice_page_too_large\"\n  | \"idempotency_key_not_allowed\"\n  | \"unauthorized\"\n  | \"forbidden_scope\"\n  | \"money_unavailable\"\n  | \"not_found\"\n  | \"method_not_allowed\"\n  | \"billing_precondition_failed\"\n  | \"conflict\"\n  | \"invoice_page_changed\"\n  | \"idempotency_key_reused\"\n  | \"idempotency_request_in_progress\"\n  | \"customer_name_taken\"\n  | \"external_id_taken\"\n  | \"ambiguous_adoption\"\n  | \"precondition_failed\"\n  | \"validation_failed\"\n  | \"batch_rejected\"\n  | \"customer_not_found\"\n  | \"template_not_found\"\n  | \"rate_not_found\"\n  | \"rate_archived\"\n  | \"rate_limited\"\n  | \"internal_error\"\n  /** No response arrived, and the request was safe to send again. */\n  | \"network_error\"\n  /** A write without an `Idempotency-Key` lost its response: reconcile first. */\n  | \"write_outcome_unknown\"\n  /** A `304` for a URL whose cached body is gone. */\n  | \"missing_cached_body\"\n  /** An error response that is not the API's envelope, such as a proxy's. */\n  | \"http_error\";\n\n/**\n * Names the rejected booking (`id`), the invalid parameter (`field`), or the\n * refused batch item (`index`, `externalId`, and the records it refers to in\n * `ids`).\n */\nexport type ErrorDetail = {\n  code: string;\n  id?: string;\n  field?: string;\n  index?: number;\n  externalId?: string;\n  ids?: string[];\n  message?: string;\n};\n\n/**\n * Every failure the client reports. Branch on `code` and `details`; the message\n * is for your log. Quote `requestId` when you contact support.\n */\nexport class ApiError extends Error {\n  readonly status: number;\n  readonly code: Open<ApiErrorCode>;\n  readonly requestId: string | null;\n  readonly details: ErrorDetail[];\n\n  constructor(failure: {\n    status: number;\n    code: Open<ApiErrorCode>;\n    requestId?: string | null;\n    details?: ErrorDetail[];\n    message?: string;\n  }) {\n    const details = failure.details ?? [];\n    super(\n      [\n        `HTTP ${failure.status}: ${failure.code}; request ID ${failure.requestId ?? \"unavailable\"}`,\n        ...(failure.message ? [failure.message] : []),\n        ...details.map(\n          (detail) =>\n            `  ${detail.id ?? detail.field ?? detail.externalId ?? \"request\"}: ${detail.code}${detail.message ? ` (${detail.message})` : \"\"}`,\n        ),\n      ].join(\"\\n\"),\n    );\n    this.name = \"ApiError\";\n    this.status = failure.status;\n    this.code = failure.code;\n    this.requestId = failure.requestId ?? null;\n    this.details = details;\n  }\n}\n\n/**\n * How long to wait before sending the same request again, or `undefined` to\n * stop. A `429` executed nothing, so even an unkeyed write repeats it. A `5xx`\n * may have executed: only a read, or a write whose `Idempotency-Key` replays\n * the stored result, is sent again.\n */\nconst retryDelay = (\n  response: Response,\n  code: string,\n  replayable: boolean,\n  attempt: number,\n): number | undefined => {\n  if (response.status === 429) {\n    const seconds = Number(response.headers.get(\"Retry-After\") ?? NaN);\n    const delay =\n      Number.isFinite(seconds) && seconds >= 0\n        ? seconds * 1000\n        : backoff(attempt);\n    // Stop rather than retry early when the requested wait is excessive.\n    return delay <= 60_000 ? delay : undefined;\n  }\n  if (!replayable) return undefined;\n  if (response.status >= 500) return backoff(attempt);\n  if (code === \"idempotency_request_in_progress\") return 2_000 * 2 ** attempt;\n  return undefined;\n};\n\nexport type ClientOptions = {\n  /** Your organization's TimeFusion origin, without `/api/integrations/v1`. */\n  origin: string;\n  apiKey: string;\n  /** Replace `fetch`, for tests or your own instrumentation. */\n  fetch?: typeof fetch;\n  sleep?: (ms: number) => Promise<void>;\n};\n\nexport type RequestOptions = {\n  /** A JSON body makes the request a `POST`; without one it is a `GET`. */\n  body?: unknown;\n  /** Revalidate a `GET` with the `ETag` cached for its URL. */\n  conditional?: boolean;\n  /**\n   * Makes a write safe to send again: the same body under the same key returns\n   * the stored result instead of executing twice.\n   */\n  idempotencyKey?: string;\n};\n\nexport type Cached = { etag: string; body: unknown };\n\n/** One client per API key: its `ETag` cache holds what that key may see. */\nexport class TimeFusionClient {\n  /** The normalized origin, such as `https://app.timefusion.at`. */\n  readonly origin: string;\n  private readonly apiKey: string;\n  private readonly fetcher: typeof fetch;\n  private readonly sleep: (ms: number) => Promise<void>;\n  private readonly cache = new Map<string, Cached>();\n  /** URLs this client requested conditionally; see `exportCache`. */\n  private readonly used = new Set<string>();\n  /** The `X-Api-Version` last seen, so each difference warns once. */\n  private apiVersion: string | null | undefined;\n\n  constructor(options: ClientOptions) {\n    const url = new URL(options.origin);\n    if (\n      url.username ||\n      url.password ||\n      url.pathname !== \"/\" ||\n      url.search ||\n      url.hash\n    )\n      throw new Error(\n        \"The TimeFusion origin must be a bare origin such as https://app.timefusion.at, without credentials, path, query or fragment.\",\n      );\n    this.origin = url.origin;\n    this.apiKey = options.apiKey;\n    this.fetcher = options.fetch ?? ((input, init) => fetch(input, init));\n    this.sleep =\n      options.sleep ??\n      ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));\n  }\n\n  // Operations, named as in the API reference -------------------------------\n\n  /**\n   * Which organization and key this is, and whether money is visible. Needs no\n   * scope.\n   */\n  async getMe(): Promise<ApiKeyContext> {\n    return (await this.request<ApiKeyContext>(\"/me\")).body;\n  }\n\n  listTimeBookings(query: TimeBookingQuery): AsyncGenerator<TimeBooking> {\n    return this.list(\"/time-bookings\", query);\n  }\n\n  /** Also returns running bookings, which lists leave out. */\n  getTimeBooking(id: string): Promise<TimeBooking> {\n    return this.get(\"/time-bookings\", id);\n  }\n\n  /**\n   * Marks or unmarks bookings as billed, all or nothing per request. `replayed`\n   * means the `Idempotency-Key` answered with an earlier execution's result.\n   */\n  async setTimeBookingsBilled(\n    request: BillingRequest,\n    options: { idempotencyKey?: string } = {},\n  ): Promise<{ result: BillingResult; replayed: boolean }> {\n    const { body, headers } = await this.request<BillingResult>(\n      \"/time-bookings/billing\",\n      { body: request, idempotencyKey: options.idempotencyKey },\n    );\n    return {\n      result: body,\n      replayed: headers.get(\"Idempotency-Replayed\") === \"true\",\n    };\n  }\n\n  listProjects(query: ProjectQuery = {}): AsyncGenerator<Project> {\n    return this.list(\"/projects\", query);\n  }\n\n  getProject(id: string): Promise<Project> {\n    return this.get(\"/projects\", id);\n  }\n\n  /** The one linked to your own id; `404` when none is. */\n  getProjectByExternalId(externalId: string): Promise<Project> {\n    return this.get(\"/projects/external\", externalId);\n  }\n\n  /** Hours booked against the budget; needs `bookings:read`. */\n  async getProjectHours(id: string): Promise<ProjectHours> {\n    return (\n      await this.request<ProjectHours>(\n        `/projects/${encodeURIComponent(id)}/hours`,\n      )\n    ).body;\n  }\n\n  listTasks(query: TaskQuery = {}): AsyncGenerator<Task> {\n    return this.list(\"/tasks\", query);\n  }\n\n  getTask(id: string): Promise<Task> {\n    return this.get(\"/tasks\", id);\n  }\n\n  listCustomers(query: CustomerQuery = {}): AsyncGenerator<Customer> {\n    return this.list(\"/customers\", query);\n  }\n\n  getCustomer(id: string): Promise<Customer> {\n    return this.get(\"/customers\", id);\n  }\n\n  /** The one linked to your own id; `404` when none is. */\n  getCustomerByExternalId(externalId: string): Promise<Customer> {\n    return this.get(\"/customers/external\", externalId);\n  }\n\n  /**\n   * Creates or updates customers by your own ids, all or nothing: one refused\n   * item is `batch_rejected` with every refused item in `details`, and nothing\n   * is written.\n   */\n  async upsertCustomers(\n    request: MasterDataBatchRequest<CustomerBatchItem>,\n    options: { idempotencyKey?: string } = {},\n  ): Promise<MasterDataBatchResult<Customer>> {\n    return (\n      await this.request<MasterDataBatchResult<Customer>>(\"/customers/batch\", {\n        body: request,\n        idempotencyKey: options.idempotencyKey,\n      })\n    ).body;\n  }\n\n  /** As `upsertCustomers`. Send the customers a project names first. */\n  async upsertProjects(\n    request: MasterDataBatchRequest<ProjectBatchItem>,\n    options: { idempotencyKey?: string } = {},\n  ): Promise<MasterDataBatchResult<Project>> {\n    return (\n      await this.request<MasterDataBatchResult<Project>>(\"/projects/batch\", {\n        body: request,\n        idempotencyKey: options.idempotencyKey,\n      })\n    ).body;\n  }\n\n  listRates(query: RateCatalogQuery = {}): AsyncGenerator<RateCatalogEntry> {\n    return this.list(\"/rates\", query);\n  }\n\n  getRate(id: string): Promise<RateCatalogEntry> {\n    return this.get(\"/rates\", id);\n  }\n\n  /** The one linked to your own id; `404` when none is. */\n  getRateByExternalId(externalId: string): Promise<RateCatalogEntry> {\n    return this.get(\"/rates/external\", externalId);\n  }\n\n  /**\n   * As `upsertCustomers`, for hourly rates. Send the rates a customer or\n   * project names by `rateExternalId` first.\n   */\n  async upsertRates(\n    request: MasterDataBatchRequest<RateBatchItem>,\n    options: { idempotencyKey?: string } = {},\n  ): Promise<MasterDataBatchResult<RateCatalogEntry>> {\n    return (\n      await this.request<MasterDataBatchResult<RateCatalogEntry>>(\n        \"/rates/batch\",\n        { body: request, idempotencyKey: options.idempotencyKey },\n      )\n    ).body;\n  }\n\n  listUsers(query: UserQuery = {}): AsyncGenerator<User> {\n    return this.list(\"/users\", query);\n  }\n\n  getUser(id: string): Promise<User> {\n    return this.get(\"/users\", id);\n  }\n\n  listInvoices(query: InvoiceQuery = {}): AsyncGenerator<Invoice> {\n    return this.list(\"/invoices\", query);\n  }\n\n  getInvoice(id: string): Promise<Invoice> {\n    return this.get(\"/invoices\", id);\n  }\n\n  // The transport the operations share --------------------------------------\n\n  /**\n   * One call to `/api/integrations/v1${path}`, retried where that is safe.\n   * Returns the headers too, for `ETag`, `X-RateLimit-*` and\n   * `Idempotency-Replayed`.\n   */\n  async request<T>(\n    path: string,\n    { body, conditional = false, idempotencyKey }: RequestOptions = {},\n  ): Promise<{ body: T; headers: Headers }> {\n    const url = `${this.origin}/api/integrations/v1${path}`;\n    const write = body !== undefined;\n    const hasIdempotencyKey = Boolean(idempotencyKey?.trim());\n    const replayable = !write || hasIdempotencyKey;\n    const conditionalRead = conditional && !write;\n    if (conditionalRead) this.used.add(url);\n    const cached = conditionalRead ? this.cache.get(url) : undefined;\n    let useTag = cached !== undefined;\n    for (let attempt = 0; ; attempt++) {\n      const last = attempt === ATTEMPTS - 1;\n      let response: Response;\n      try {\n        response = await this.fetcher(url, {\n          method: write ? \"POST\" : \"GET\",\n          redirect: \"error\",\n          signal: AbortSignal.timeout(TIMEOUT_MS[write ? \"write\" : \"read\"]),\n          headers: {\n            Authorization: `Bearer ${this.apiKey}`,\n            ...(write ? { \"Content-Type\": \"application/json\" } : {}),\n            ...(useTag && cached ? { \"If-None-Match\": cached.etag } : {}),\n            ...(hasIdempotencyKey\n              ? { \"Idempotency-Key\": idempotencyKey! }\n              : {}),\n          },\n          ...(write ? { body: JSON.stringify(body) } : {}),\n        });\n      } catch {\n        // A write without a key that lost its response may have executed:\n        // reconcile before sending it again, never automatically.\n        if (!replayable || last)\n          throw new ApiError({\n            status: 0,\n            code: write ? \"write_outcome_unknown\" : \"network_error\",\n          });\n        await this.sleep(backoff(attempt));\n        continue;\n      }\n      if (response.ok || response.status === 304)\n        this.checkApiVersion(response.headers.get(\"X-Api-Version\"));\n      const requestId = response.headers.get(\"X-Request-Id\");\n      if (response.status === 304) {\n        if (useTag && cached)\n          return { body: cached.body as T, headers: response.headers };\n        useTag = false;\n        if (last)\n          throw new ApiError({\n            status: 304,\n            code: \"missing_cached_body\",\n            requestId,\n          });\n        continue;\n      }\n      if (!response.ok) {\n        const { error } = (await response.json().catch(() => ({}))) as {\n          error?: {\n            code?: Open<ApiErrorCode>;\n            message?: string;\n            details?: ErrorDetail[];\n            requestId?: string;\n          };\n        };\n        const failure = new ApiError({\n          status: response.status,\n          code: error?.code ?? \"http_error\",\n          requestId: requestId ?? error?.requestId,\n          details: error?.details,\n          message: error?.message,\n        });\n        const delay = retryDelay(response, failure.code, replayable, attempt);\n        if (last || delay === undefined) throw failure;\n        await this.sleep(delay);\n        continue;\n      }\n      const result: unknown = await response.json();\n      if (conditionalRead) {\n        const etag = response.headers.get(\"ETag\");\n        if (etag) this.cache.set(url, { etag, body: result });\n        else this.cache.delete(url);\n      }\n      return { body: result as T, headers: response.headers };\n    }\n  }\n\n  /**\n   * Warns once per version the server reports that is not\n   * `EXPECTED_API_VERSION`, patch releases included, and once if a successful\n   * response names none (a proxy stripping headers, or not TimeFusion). Never\n   * blocks: v1 only ever adds, so this copy keeps working meanwhile.\n   */\n  private checkApiVersion(version: string | null): void {\n    if (version === this.apiVersion) return;\n    this.apiVersion = version;\n    if (version === EXPECTED_API_VERSION) return;\n    // eslint-disable-next-line no-console -- the one signal a copy is outdated.\n    console.warn(\n      version\n        ? `TimeFusion API ${version} is live; this client was written against ${EXPECTED_API_VERSION}. Review the changes at ${this.origin}/api/integrations/v1/docs and update your copy from ${this.origin}/api/integrations/v1/starter-kit.tar.gz.`\n        : `TimeFusion API sent no X-Api-Version header, so this client cannot confirm it still matches ${EXPECTED_API_VERSION}. Check ${this.origin}/api/integrations/v1/docs, and whether a proxy strips response headers.`,\n    );\n  }\n\n  /**\n   * Every page of a list, following `nextCursor` to the end. Keep the filters\n   * unchanged while it runs. `conditional` revalidates each page with the\n   * `ETag` cached for its URL, and a `304` reuses the cached page, cursor\n   * included.\n   */\n  async *pages<T>(\n    path: string,\n    query: Query = {},\n    { conditional = false }: { conditional?: boolean } = {},\n  ): AsyncGenerator<Page<T>> {\n    const params = toParams({\n      ...query,\n      limit: query.limit ?? LIMITS.pageSize,\n    });\n    params.delete(\"cursor\");\n    const seen = new Set<string>();\n    while (true) {\n      const { body: page } = await this.request<Page<T>>(`${path}?${params}`, {\n        conditional,\n      });\n      if (\n        !Array.isArray(page.data) ||\n        typeof page.pagination?.hasMore !== \"boolean\"\n      )\n        throw new Error(\"Invalid page response.\");\n      yield page;\n      if (!page.pagination.hasMore) return;\n      const next = page.pagination.nextCursor;\n      if (!next || seen.has(next))\n        throw new Error(\"Missing or repeated pagination cursor.\");\n      seen.add(next);\n      params.set(\"cursor\", next);\n    }\n  }\n\n  /** Every item of a list, across all pages. */\n  async *list<T>(path: string, query: Query = {}): AsyncGenerator<T> {\n    for await (const page of this.pages<T>(path, query)) yield* page.data;\n  }\n\n  private async get<T>(collection: string, id: string): Promise<T> {\n    return (await this.request<T>(`${collection}/${encodeURIComponent(id)}`))\n      .body;\n  }\n\n  /**\n   * The `ETag`s and bodies of the pages requested with `conditional`, to save\n   * beside your synced data and hand to `importCache` on the next run. That is\n   * where unchanged pages become cheap: one run never asks for a URL twice.\n   * Only URLs this client requested are exported, so a moving window does not\n   * grow the saved cache forever.\n   */\n  exportCache(): Record<string, Cached> {\n    return Object.fromEntries(\n      [...this.cache].filter(([url]) => this.used.has(url)),\n    );\n  }\n\n  importCache(entries: Record<string, Cached>): void {\n    for (const [url, entry] of Object.entries(entries))\n      this.cache.set(url, entry);\n  }\n}\n\n/**\n * Collects a list by id, keeping the last copy: a row edited while the walk\n * runs can move behind the cursor and arrive twice.\n */\nexport async function collectById<T extends { id: string }>(\n  items: AsyncIterable<T>,\n): Promise<T[]> {\n  const rows = new Map<string, T>();\n  for await (const item of items) rows.set(item.id, item);\n  return [...rows.values()];\n}\n\n/** Local bookings by id, and the highest `updatedAt` a completed walk returned. */\nexport type Mirror = {\n  checkpoint: string | null;\n  rows: Record<string, TimeBooking>;\n};\n\n/**\n * Walks one window into a copy of `previous`. Save the result atomically, and\n * only once this returns: a failed walk replays its window from the start. Rows\n * are upserted and never deleted by absence, because a walk is not a snapshot.\n */\nexport async function syncWindow(\n  client: TimeFusionClient,\n  previous: Mirror,\n  query: TimeBookingQuery,\n  { conditional = true }: { conditional?: boolean } = {},\n): Promise<Mirror> {\n  const rows = { ...previous.rows };\n  let checkpoint = previous.checkpoint;\n  for await (const page of client.pages<TimeBooking>(\"/time-bookings\", query, {\n    conditional,\n  })) {\n    for (const row of page.data) {\n      // Upsert even when `updatedAt` did not move: a renamed project changes\n      // the body without touching the booking.\n      rows[row.id] = row;\n      if (\n        checkpoint === null ||\n        Date.parse(row.updatedAt) > Date.parse(checkpoint)\n      )\n        checkpoint = row.updatedAt;\n    }\n  }\n  return { checkpoint, rows };\n}\n\n/** The four master-data lists a mirror keeps in step with its bookings. */\nexport type MasterData = {\n  projects: Record<string, Project>;\n  tasks: Record<string, Task>;\n  customers: Record<string, Customer>;\n  rates: Record<string, RateCatalogEntry>;\n};\n\nexport const emptyMasterData = (): MasterData => ({\n  projects: {},\n  tasks: {},\n  customers: {},\n  rates: {},\n});\n\n/**\n * Walks the master-data lists by last modification, upserting into a copy of\n * `previous`, except `/rates`, which is read whole (below). Save the result\n * atomically, like `syncWindow`.\n *\n * **All four, not one.** `Project.updatedAt` does not move when its customer is\n * renamed or its rate edited, and `Task.updatedAt` not when an ancestor or its\n * rate changes (D-32.5), so a mirror walking bookings alone misses those. Each\n * change appears on the list that owns it, and one window serves every walked\n * list: the same `from`/`to` the booking walk uses, so the whole sync stays one\n * loop.\n *\n * Leave `from` out on a mirror's first read: the lists are small, and a row\n * last written before the booking checkpoint would otherwise never arrive.\n *\n * Projects, tasks and customers are upserted by id and never deleted by\n * absence, exactly as `syncWindow`: a walk is not a snapshot, and the API\n * publishes no tombstones. `sort=updatedAt` makes `from`/`to` select by last\n * modification; under the default ordering the API rejects them with `400\n * invalid_parameters`.\n *\n * **Rates are the exception: read whole every run.** `isOrganizationDefault`\n * flips when the organization changes its default, a write to the organization\n * row that moves no rate's `updatedAt`, so a windowed walk would leave the old\n * default flagged. The catalog is a handful of rows, and an unfiltered read is\n * a snapshot, so it replaces the mirrored rates and drops a deleted one too.\n */\nexport async function syncMasterData(\n  client: TimeFusionClient,\n  previous: MasterData,\n  window: { from?: string; to?: string } = {},\n): Promise<MasterData> {\n  const query = { ...window, sort: \"updatedAt\" as const };\n  const collect = async <T extends { id: string }>(\n    items: AsyncIterable<T>,\n    into: Record<string, T>,\n  ): Promise<Record<string, T>> => {\n    for await (const item of items) into[item.id] = item;\n    return into;\n  };\n\n  const projects = { ...previous.projects };\n  const tasks = { ...previous.tasks };\n  const customers = { ...previous.customers };\n  await collect(client.listProjects(query), projects);\n  await collect(client.listTasks(query), tasks);\n  await collect(client.listCustomers(query), customers);\n  const rates = await collect(client.listRates(), {});\n\n  return { projects, tasks, customers, rates };\n}"}]}},"/api/integrations/v1/time-bookings/{id}":{"get":{"operationId":"getTimeBooking","tags":["Time bookings"],"summary":"Fetch one time booking by id","description":"Required scope: `bookings:read`. A single time booking by id. Unlike `GET /time-bookings`, this also returns a tracked booking that is still running; an unknown id returns `404`.","x-required-scopes":["bookings:read"],"x-badges":[{"name":"bookings:read","position":"before"}],"x-scalar-stability":"stable","parameters":[{"schema":{"type":"string","format":"uuid","description":"Stable identifier of the time booking to retrieve."},"required":true,"description":"Stable identifier of the time booking to retrieve.","name":"id","in":"path"}],"responses":{"200":{"description":"The time booking.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"ETag":{"description":"Strong validator over this exact body. Send it back as `If-None-Match` on the next poll and an unchanged page answers `304` with no body. It covers the whole request — filters, cursor, and whether this key may see money — so tags from two different keys or two different queries never match.","required":true,"schema":{"type":"string","example":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\""}},"Cache-Control":{"description":"`no-cache` means revalidate before reuse, not do not store — it is what makes a conditional request happen. `private` keeps a body scoped to one API key out of shared caches.","required":true,"schema":{"type":"string","example":"private, no-cache"}},"Vary":{"description":"The response depends on the key that asked: two keys for the same organization can see different amounts on the same URL.","required":true,"schema":{"type":"string","example":"Authorization"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TimeBooking"}}}},"304":{"description":"The representation is unchanged since the `ETag` in `If-None-Match`. No body. This still counts against the rate limit — the server ran the query and built the response; only the transfer was skipped.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"ETag":{"description":"Strong validator over this exact body. Send it back as `If-None-Match` on the next poll and an unchanged page answers `304` with no body. It covers the whole request — filters, cursor, and whether this key may see money — so tags from two different keys or two different queries never match.","required":true,"schema":{"type":"string","example":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\""}}}},"400":{"description":"The request was rejected before it ran. `error.code` says why:\n\n- `invalid_parameters` — a query, path or body value does not match the schema. Fix the fields `details[]` names.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"invalid_parameters","message":"Invalid request parameters.","details":[{"field":"limit","code":"too_big","message":"`limit` must be at most 200."}]}},"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"401":{"description":"The key is missing, unknown, revoked or expired, or the organization has switched the Integrations API off, and the response deliberately does not say which. Send a valid key as `Authorization: Bearer sk_tf_int_live_…`. If a key that worked before answers `401`, have an administrator check that it was not revoked or expired and that the API is still switched on.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"WWW-Authenticate":{"description":"Bearer authentication challenge returned on every 401.","required":true,"schema":{"type":"string","example":"Bearer"}}},"content":{"application/json":{"example":{"error":{"code":"unauthorized","message":"Missing or invalid API key. Send it as `Authorization: Bearer sk_tf_int_live_…`."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key is missing the `bookings:read` scope this endpoint requires. Scopes are fixed at key creation, so issue a new key carrying the scope named in the message.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"forbidden_scope","message":"This API key is missing the required scope `bookings:read`. Scopes are fixed at key creation — issue a new key to change them."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Time booking was not found.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"not_found","message":"Time booking was not found."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"The per-key budget for this window is spent. Failed-authentication throttling carries no per-key `X-RateLimit-*` headers. Wait for `Retry-After`, a delta in seconds, then retry.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"Retry-After":{"description":"Minimum delay before retrying, in seconds (a delta, not Unix epoch seconds).","required":true,"schema":{"type":"integer","minimum":1,"example":30}}},"content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"An unexpected error, with a `requestId` for support. Quote the `requestId` (or `X-Request-Id`) in a support request.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"internal_error","message":"An unexpected error occurred. Quote the request ID when contacting support.","requestId":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/integrations/v1/projects":{"get":{"operationId":"listProjects","tags":["Projects"],"summary":"List projects","description":"Required scope: `masterdata:read`. Oldest first by creation, in pages walked with `cursor`. `?sort=updatedAt` walks by last modification instead, with `from`/`to` bounding that walk. Deactivated projects are returned and flagged, not hidden: a project deactivated in September still has August bookings on an August invoice.","x-required-scopes":["masterdata:read"],"x-badges":[{"name":"masterdata:read","position":"before"}],"x-scalar-stability":"stable","parameters":[{"schema":{"type":"array","items":{"type":"string","format":"uuid"},"minItems":1,"maxItems":100,"description":"Only the projects named here, by TimeFusion's `id`: the list form of `GET /projects/{id}`, for resolving many at once. An unknown id contributes no rows rather than an error. Repeat the parameter for several values — `?id=a&id=b` — and a project matches any of them; at most 100 per request. Repeated values within one parameter are ORed, different parameters are ANDed, and duplicates are ignored. Bracketed keys (`id[]`, `id[0]`) are rejected with `400`.","example":["11111111-1111-4111-8111-111111111111","22222222-2222-4222-8222-222222222222"]},"required":false,"description":"Only the projects named here, by TimeFusion's `id`: the list form of `GET /projects/{id}`, for resolving many at once. An unknown id contributes no rows rather than an error. Repeat the parameter for several values — `?id=a&id=b` — and a project matches any of them; at most 100 per request. Repeated values within one parameter are ORed, different parameters are ANDed, and duplicates are ignored. Bracketed keys (`id[]`, `id[0]`) are rejected with `400`.","name":"id","in":"query","style":"form","explode":true},{"schema":{"type":"array","items":{"type":"string","pattern":"^[\\x21-\\x7e](?:[\\x20-\\x7e]{0,126}[\\x21-\\x7e])?$"},"minItems":1,"maxItems":50,"description":"Only projects linked to this `externalId`, matched exactly (case-sensitive). Repeat the parameter for several values — `?externalId=P-2026-001&externalId=P-2026-002` — and a project matches any of them; at most 50 per request. Repeated values within one parameter are ORed, different parameters are ANDed, and duplicates are ignored. Bracketed keys (`externalId[]`, `externalId[0]`) are rejected with `400`.","example":["P-2026-001","P-2026-002"]},"required":false,"description":"Only projects linked to this `externalId`, matched exactly (case-sensitive). Repeat the parameter for several values — `?externalId=P-2026-001&externalId=P-2026-002` — and a project matches any of them; at most 50 per request. Repeated values within one parameter are ORed, different parameters are ANDed, and duplicates are ignored. Bracketed keys (`externalId[]`, `externalId[0]`) are rejected with `400`.","name":"externalId","in":"query","style":"form","explode":true},{"schema":{"type":"array","items":{"type":"string","format":"uuid"},"minItems":1,"maxItems":50,"description":"Only projects of this customer. Repeat the parameter for several values — `?customerId=a&customerId=b` — and a project matches any of them; at most 50 per request. Repeated values within one parameter are ORed, different parameters are ANDed, and duplicates are ignored. Bracketed keys (`customerId[]`, `customerId[0]`) are rejected with `400`.","example":["11111111-1111-4111-8111-111111111111","22222222-2222-4222-8222-222222222222"]},"required":false,"description":"Only projects of this customer. Repeat the parameter for several values — `?customerId=a&customerId=b` — and a project matches any of them; at most 50 per request. Repeated values within one parameter are ORed, different parameters are ANDed, and duplicates are ignored. Bracketed keys (`customerId[]`, `customerId[0]`) are rejected with `400`.","name":"customerId","in":"query","style":"form","explode":true},{"schema":{"type":"string","enum":["true","false"],"description":"Filter by the deactivated flag. Omit to receive both."},"required":false,"description":"Filter by the deactivated flag. Omit to receive both.","name":"deactivated","in":"query"},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}([T ]\\d{2}:\\d{2}(:\\d{2}(\\.\\d{1,3})?)?(Z|[+-]\\d{2}:\\d{2})?)?$","description":"Inclusive start of the walk. **Only valid with `sort=updatedAt`**, where it selects rows last written at or after this instant; rejected as `400 invalid_parameters` with the default `sort=createdAt`. A date without a time means 00:00:00.000 UTC on that day. Timestamps without an offset are interpreted as UTC; explicit offsets are respected.","example":"2026-08-01"},"required":false,"description":"Inclusive start of the walk. **Only valid with `sort=updatedAt`**, where it selects rows last written at or after this instant; rejected as `400 invalid_parameters` with the default `sort=createdAt`. A date without a time means 00:00:00.000 UTC on that day. Timestamps without an offset are interpreted as UTC; explicit offsets are respected.","name":"from","in":"query"},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}([T ]\\d{2}:\\d{2}(:\\d{2}(\\.\\d{1,3})?)?(Z|[+-]\\d{2}:\\d{2})?)?$","description":"Inclusive end of the walk. **Only valid with `sort=updatedAt`**, where it selects rows last written at or before this instant; rejected as `400 invalid_parameters` with the default `sort=createdAt`. A date without a time means 23:59:59.999 UTC on that day. Timestamps without an offset are interpreted as UTC; explicit offsets are respected.","example":"2026-08-31"},"required":false,"description":"Inclusive end of the walk. **Only valid with `sort=updatedAt`**, where it selects rows last written at or before this instant; rejected as `400 invalid_parameters` with the default `sort=createdAt`. A date without a time means 23:59:59.999 UTC on that day. Timestamps without an offset are interpreted as UTC; explicit offsets are respected.","name":"to","in":"query"},{"schema":{"type":"string","enum":["createdAt","updatedAt"],"default":"createdAt","description":"Which order to walk, and which timestamp `from`/`to` bound. `createdAt` (default) walks oldest-created first. `updatedAt` walks by last modification, oldest first — the ordering a mirror polls to find which rows changed. `from` and `to` are only accepted with `updatedAt`. Cursors are not interchangeable between the two.","example":"createdAt"},"required":false,"description":"Which order to walk, and which timestamp `from`/`to` bound. `createdAt` (default) walks oldest-created first. `updatedAt` walks by last modification, oldest first — the ordering a mirror polls to find which rows changed. `from` and `to` are only accepted with `updatedAt`. Cursors are not interchangeable between the two.","name":"sort","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":200,"default":50,"description":"Page size, 1–200. Values above 200 are rejected, not clamped.","example":50},"required":false,"description":"Page size, 1–200. Values above 200 are rejected, not clamped.","name":"limit","in":"query"},{"schema":{"type":"string","description":"Opaque cursor from a previous page's `pagination.nextCursor` **on this endpoint**. Cursors are not interchangeable between endpoints; one issued elsewhere is rejected as `invalid_cursor`. Omit for the first page."},"required":false,"description":"Opaque cursor from a previous page's `pagination.nextCursor` **on this endpoint**. Cursors are not interchangeable between endpoints; one issued elsewhere is rejected as `invalid_cursor`. Omit for the first page.","name":"cursor","in":"query"}],"responses":{"200":{"description":"One page of projects.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"ETag":{"description":"Strong validator over this exact body. Send it back as `If-None-Match` on the next poll and an unchanged page answers `304` with no body. It covers the whole request — filters, cursor, and whether this key may see money — so tags from two different keys or two different queries never match.","required":true,"schema":{"type":"string","example":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\""}},"Cache-Control":{"description":"`no-cache` means revalidate before reuse, not do not store — it is what makes a conditional request happen. `private` keeps a body scoped to one API key out of shared caches.","required":true,"schema":{"type":"string","example":"private, no-cache"}},"Vary":{"description":"The response depends on the key that asked: two keys for the same organization can see different amounts on the same URL.","required":true,"schema":{"type":"string","example":"Authorization"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectPage"}}}},"304":{"description":"The representation is unchanged since the `ETag` in `If-None-Match`. No body. This still counts against the rate limit — the server ran the query and built the response; only the transfer was skipped.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"ETag":{"description":"Strong validator over this exact body. Send it back as `If-None-Match` on the next poll and an unchanged page answers `304` with no body. It covers the whole request — filters, cursor, and whether this key may see money — so tags from two different keys or two different queries never match.","required":true,"schema":{"type":"string","example":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\""}}}},"400":{"description":"The request was rejected before it ran. `error.code` says why:\n\n- `invalid_parameters` — a query, path or body value does not match the schema. Fix the fields `details[]` names.\n- `invalid_period` — `from` or `to` is date-shaped but not a real date (`2026-13-01`, `2026-02-31`), or `to` is earlier than `from`. A boundary that is not date-shaped at all fails the schema as `invalid_parameters` instead. Send a real calendar date, with `to` no earlier than `from`.\n- `invalid_cursor` — the `cursor` was not issued by this API. Discard the cursor and restart the walk from the first page.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"invalid_parameters","message":"Invalid request parameters.","details":[{"field":"limit","code":"too_big","message":"`limit` must be at most 200."}]}},"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"401":{"description":"The key is missing, unknown, revoked or expired, or the organization has switched the Integrations API off, and the response deliberately does not say which. Send a valid key as `Authorization: Bearer sk_tf_int_live_…`. If a key that worked before answers `401`, have an administrator check that it was not revoked or expired and that the API is still switched on.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"WWW-Authenticate":{"description":"Bearer authentication challenge returned on every 401.","required":true,"schema":{"type":"string","example":"Bearer"}}},"content":{"application/json":{"example":{"error":{"code":"unauthorized","message":"Missing or invalid API key. Send it as `Authorization: Bearer sk_tf_int_live_…`."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key is missing the `masterdata:read` scope this endpoint requires. Scopes are fixed at key creation, so issue a new key carrying the scope named in the message.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"forbidden_scope","message":"This API key is missing the required scope `masterdata:read`. Scopes are fixed at key creation — issue a new key to change them."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"The per-key budget for this window is spent. Failed-authentication throttling carries no per-key `X-RateLimit-*` headers. Wait for `Retry-After`, a delta in seconds, then retry.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"Retry-After":{"description":"Minimum delay before retrying, in seconds (a delta, not Unix epoch seconds).","required":true,"schema":{"type":"integer","minimum":1,"example":30}}},"content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"An unexpected error, with a `requestId` for support. Quote the `requestId` (or `X-Request-Id`) in a support request.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"internal_error","message":"An unexpected error occurred. Quote the request ID when contacting support.","requestId":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"createProject","tags":["Projects"],"summary":"Create a project","description":"Required scope: `projects:write`. Creates a project, attributed to this key, and answers with it as `GET /projects/{id}` returns it, plus its `ETag`. With `templateProjectId` it starts as a copy of that project's members and tasks; without it the project has no members and no tasks. Send an `Idempotency-Key` to make a retry safe: without one, a second identical request is a second project. To link the project to an id of your own system, use `PUT /projects/external/{externalId}` instead, which is safe to send twice without a key.","x-required-scopes":["projects:write"],"x-badges":[{"name":"projects:write","position":"before"}],"x-scalar-stability":"stable","parameters":[{"schema":{"type":"string","pattern":"^[\\x20-\\x7e]{1,128}$","description":"Optional: 1–128 printable ASCII characters, case-sensitive. Send a unique key — a UUID is ideal — to make this write safely retryable: if you never see the response, send the **identical** request with the **same** key and the original result is returned instead of executing again. Keys are scoped to the organization and operation; any currently authorized API key in that organization may replay the result. Receipts are retained for 7 days from execution and describe that original execution, not the records' current state; a key without `money:read` is shown it without rates. A replay carries `Idempotency-Replayed: true`. The same key with a different body is `409 idempotency_key_reused`. Without this header a retry creates a second record.","example":"3f8c1b9e-2d47-4a1e-9a1b-5f6f0f7c2a10"},"required":false,"description":"Optional: 1–128 printable ASCII characters, case-sensitive. Send a unique key — a UUID is ideal — to make this write safely retryable: if you never see the response, send the **identical** request with the **same** key and the original result is returned instead of executing again. Keys are scoped to the organization and operation; any currently authorized API key in that organization may replay the result. Receipts are retained for 7 days from execution and describe that original execution, not the records' current state; a key without `money:read` is shown it without rates. A replay carries `Idempotency-Replayed: true`. The same key with a different body is `409 idempotency_key_reused`. Without this header a retry creates a second record.","name":"Idempotency-Key","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectCreateRequest"}}}},"responses":{"201":{"description":"The project was created. With `Idempotency-Replayed: true`, this is the retained answer of an earlier execution: the project as it was then.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"ETag":{"description":"Strong validator of the record as `GET /{id}` returns it for this key. Send it as `If-Match` on the next write to make that write conditional.","required":true,"schema":{"type":"string","example":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\""}},"Idempotency-Replayed":{"description":"Sent as `true` when this response was replayed from a retained receipt rather than executed. Absent on a first execution. The body is the original one, so it describes that execution and not the records' current state.","schema":{"type":"string","example":"true"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Project"}}}},"400":{"description":"The request was rejected before it ran. `error.code` says why:\n\n- `malformed_json` — the request body is not valid JSON. Check the payload and the `Content-Type` header.\n- `invalid_parameters` — a query, path or body value does not match the schema. Fix the fields `details[]` names.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"invalid_parameters","message":"Invalid request parameters.","details":[{"field":"rateId","code":"invalid_format","message":"`rateId` must be a UUID."}]}},"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"401":{"description":"The key is missing, unknown, revoked or expired, or the organization has switched the Integrations API off, and the response deliberately does not say which. Send a valid key as `Authorization: Bearer sk_tf_int_live_…`. If a key that worked before answers `401`, have an administrator check that it was not revoked or expired and that the API is still switched on.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"WWW-Authenticate":{"description":"Bearer authentication challenge returned on every 401.","required":true,"schema":{"type":"string","example":"Bearer"}}},"content":{"application/json":{"example":{"error":{"code":"unauthorized","message":"Missing or invalid API key. Send it as `Authorization: Bearer sk_tf_int_live_…`."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key is missing the `projects:write` scope this endpoint requires. Scopes are fixed at key creation, so issue a new key carrying the scope named in the message. Also returned as `money_unavailable` when the body sends `rateId` or `rateExternalId` while the key may not see money: without `money:read`, or with the organization's accounting feature switched off. `details[]` says which.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"forbidden_scope","message":"This API key is missing the required scope `projects:write`. Scopes are fixed at key creation — issue a new key to change them."}},"schema":{"anyOf":[{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/MoneyUnavailableError"}]}}}},"409":{"description":"The write was refused, so nothing was written. `error.code` says why:\n\n- `idempotency_key_reused` — this `Idempotency-Key` is retained against a different request body. Receipts are kept for 7 days. Send the original request unchanged, or a new key for a new intent.\n- `idempotency_request_in_progress` — another request holding this key is still running and did not finish within the bounded wait. **Retry** the identical request after a few seconds; it replays that request's result rather than executing twice.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"idempotency_key_reused","message":"This `Idempotency-Key` was already used for a different request body. Retry the original request unchanged, or generate a new key for the new request."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"The request is well-formed but names something this organization cannot use here, so nothing was written. `error.code` says why:\n\n- `customer_not_found` — `customerId` or `customerExternalId` names no customer of this organization. Check the reference; when the customer is new, create it first.\n- `template_not_found` — `templateProjectId` names no project of this organization. Check the id, or create the project without a template.\n- `rate_not_found` — `rateId` or `rateExternalId` names no hourly rate of this organization. Check the reference against `GET /rates`; when the rate is new, create it first (`PUT /rates/external/{externalId}`).\n- `rate_archived` — `rateId` or `rateExternalId` names an archived rate, which cannot be newly assigned. A record already carrying it may keep it. Choose an active rate (`GET /rates?archived=false`).","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"customer_not_found","message":"`customerId` or `customerExternalId` names no customer of this organization."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"The per-key budget for this window is spent. Failed-authentication throttling carries no per-key `X-RateLimit-*` headers. Wait for `Retry-After`, a delta in seconds, then retry.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"Retry-After":{"description":"Minimum delay before retrying, in seconds (a delta, not Unix epoch seconds).","required":true,"schema":{"type":"integer","minimum":1,"example":30}}},"content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"An unexpected error, with a `requestId` for support. Quote the `requestId` (or `X-Request-Id`) in a support request.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"internal_error","message":"An unexpected error occurred. Quote the request ID when contacting support.","requestId":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/integrations/v1/projects/{id}":{"get":{"operationId":"getProject","tags":["Projects"],"summary":"Fetch one project by id","description":"Required scope: `masterdata:read`. A single project by id. An unknown id returns `404`.","x-required-scopes":["masterdata:read"],"x-badges":[{"name":"masterdata:read","position":"before"}],"x-scalar-stability":"stable","parameters":[{"schema":{"type":"string","format":"uuid","description":"Stable identifier of the project to retrieve."},"required":true,"description":"Stable identifier of the project to retrieve.","name":"id","in":"path"}],"responses":{"200":{"description":"The project.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"ETag":{"description":"Strong validator over this exact body. Send it back as `If-None-Match` on the next poll and an unchanged page answers `304` with no body. It covers the whole request — filters, cursor, and whether this key may see money — so tags from two different keys or two different queries never match.","required":true,"schema":{"type":"string","example":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\""}},"Cache-Control":{"description":"`no-cache` means revalidate before reuse, not do not store — it is what makes a conditional request happen. `private` keeps a body scoped to one API key out of shared caches.","required":true,"schema":{"type":"string","example":"private, no-cache"}},"Vary":{"description":"The response depends on the key that asked: two keys for the same organization can see different amounts on the same URL.","required":true,"schema":{"type":"string","example":"Authorization"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Project"}}}},"304":{"description":"The representation is unchanged since the `ETag` in `If-None-Match`. No body. This still counts against the rate limit — the server ran the query and built the response; only the transfer was skipped.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"ETag":{"description":"Strong validator over this exact body. Send it back as `If-None-Match` on the next poll and an unchanged page answers `304` with no body. It covers the whole request — filters, cursor, and whether this key may see money — so tags from two different keys or two different queries never match.","required":true,"schema":{"type":"string","example":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\""}}}},"400":{"description":"The request was rejected before it ran. `error.code` says why:\n\n- `invalid_parameters` — a query, path or body value does not match the schema. Fix the fields `details[]` names.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"invalid_parameters","message":"Invalid request parameters.","details":[{"field":"limit","code":"too_big","message":"`limit` must be at most 200."}]}},"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"401":{"description":"The key is missing, unknown, revoked or expired, or the organization has switched the Integrations API off, and the response deliberately does not say which. Send a valid key as `Authorization: Bearer sk_tf_int_live_…`. If a key that worked before answers `401`, have an administrator check that it was not revoked or expired and that the API is still switched on.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"WWW-Authenticate":{"description":"Bearer authentication challenge returned on every 401.","required":true,"schema":{"type":"string","example":"Bearer"}}},"content":{"application/json":{"example":{"error":{"code":"unauthorized","message":"Missing or invalid API key. Send it as `Authorization: Bearer sk_tf_int_live_…`."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key is missing the `masterdata:read` scope this endpoint requires. Scopes are fixed at key creation, so issue a new key carrying the scope named in the message.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"forbidden_scope","message":"This API key is missing the required scope `masterdata:read`. Scopes are fixed at key creation — issue a new key to change them."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Project was not found.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"not_found","message":"Project was not found."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"The per-key budget for this window is spent. Failed-authentication throttling carries no per-key `X-RateLimit-*` headers. Wait for `Retry-After`, a delta in seconds, then retry.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"Retry-After":{"description":"Minimum delay before retrying, in seconds (a delta, not Unix epoch seconds).","required":true,"schema":{"type":"integer","minimum":1,"example":30}}},"content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"An unexpected error, with a `requestId` for support. Quote the `requestId` (or `X-Request-Id`) in a support request.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"internal_error","message":"An unexpected error occurred. Quote the request ID when contacting support.","requestId":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"operationId":"updateProject","tags":["Projects"],"summary":"Update a project by id","description":"Required scope: `projects:write`. Changes the fields the body sends and leaves the others as they are; `null` clears a nullable field. A body that changes nothing writes nothing: `updatedAt` and the `ETag` stay put, which is what keeps two systems syncing in both directions from echoing one change back and forth. Otherwise the change is attributed to this key. Deactivating and reactivating are the `deactivated` field, with the app's side effects. `externalId` links, re-links or (with `null`) unlinks the project. A `PATCH` takes no entity tag: to refuse the write when the project changed since you read it, send its `ETag` as `If-Match` to the upsert by `externalId` instead.","x-required-scopes":["projects:write"],"x-badges":[{"name":"projects:write","position":"before"}],"x-scalar-stability":"stable","parameters":[{"schema":{"type":"string","format":"uuid","description":"Stable identifier of the project to retrieve."},"required":true,"description":"Stable identifier of the project to retrieve.","name":"id","in":"path"},{"schema":{"type":"string","pattern":"^\\*$","description":"Only `*`, which always holds here. Any entity tag is `400`, and nothing is written: our hosting's CDN would answer a successful conditional `PATCH` with a `412` of its own after the write ran. To update only the representation you saw, send its tag as `If-Match` to the upsert, `PUT /{collection}/external/{externalId}`.","example":"*"},"required":false,"description":"Only `*`, which always holds here. Any entity tag is `400`, and nothing is written: our hosting's CDN would answer a successful conditional `PATCH` with a `412` of its own after the write ran. To update only the representation you saw, send its tag as `If-Match` to the upsert, `PUT /{collection}/external/{externalId}`.","name":"If-Match","in":"header"},{"schema":{"type":"string","description":"Not taken: any value is `400`, and nothing is written. The record exists by its id, so `*` could never pass. To update only the representation you saw, send its tag as `If-Match` to the upsert, `PUT /{collection}/external/{externalId}`."},"required":false,"description":"Not taken: any value is `400`, and nothing is written. The record exists by its id, so `*` could never pass. To update only the representation you saw, send its tag as `If-Match` to the upsert, `PUT /{collection}/external/{externalId}`.","name":"If-None-Match","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectPatchRequest"}}}},"responses":{"200":{"description":"The project as it now is, whether or not anything changed.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"ETag":{"description":"Strong validator of the record as `GET /{id}` returns it for this key. Send it as `If-Match` on the next write to make that write conditional.","required":true,"schema":{"type":"string","example":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\""}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Project"}}}},"400":{"description":"The request was rejected before it ran. `error.code` says why:\n\n- `malformed_json` — the request body is not valid JSON. Check the payload and the `Content-Type` header.\n- `invalid_parameters` — a query, path or body value does not match the schema. Fix the fields `details[]` names.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"invalid_parameters","message":"Invalid request parameters.","details":[{"field":"rateId","code":"invalid_format","message":"`rateId` must be a UUID."}]}},"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"401":{"description":"The key is missing, unknown, revoked or expired, or the organization has switched the Integrations API off, and the response deliberately does not say which. Send a valid key as `Authorization: Bearer sk_tf_int_live_…`. If a key that worked before answers `401`, have an administrator check that it was not revoked or expired and that the API is still switched on.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"WWW-Authenticate":{"description":"Bearer authentication challenge returned on every 401.","required":true,"schema":{"type":"string","example":"Bearer"}}},"content":{"application/json":{"example":{"error":{"code":"unauthorized","message":"Missing or invalid API key. Send it as `Authorization: Bearer sk_tf_int_live_…`."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key is missing the `projects:write` scope this endpoint requires. Scopes are fixed at key creation, so issue a new key carrying the scope named in the message. Also returned as `money_unavailable` when the body sends `rateId` or `rateExternalId` while the key may not see money: without `money:read`, or with the organization's accounting feature switched off. `details[]` says which.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"forbidden_scope","message":"This API key is missing the required scope `projects:write`. Scopes are fixed at key creation — issue a new key to change them."}},"schema":{"anyOf":[{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/MoneyUnavailableError"}]}}}},"404":{"description":"Project was not found.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"not_found","message":"Project was not found."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"The write was refused, so nothing was written. `error.code` says why:\n\n- `external_id_taken` — a `PATCH` sent an `externalId` already linked to another record of the same type. Nothing was written. Clear it on the other record first (`PATCH` with `externalId: null`), or send a different one. `GET /customers?externalId=…`, `GET /projects?externalId=…` or `GET /rates?externalId=…` finds the holder.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"external_id_taken","message":"This `externalId` is already linked to another record of this type. Unlink it there first, or send a different one."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"The request is well-formed but names something this organization cannot use here, so nothing was written. `error.code` says why:\n\n- `customer_not_found` — `customerId` or `customerExternalId` names no customer of this organization. Check the reference; when the customer is new, create it first.\n- `rate_not_found` — `rateId` or `rateExternalId` names no hourly rate of this organization. Check the reference against `GET /rates`; when the rate is new, create it first (`PUT /rates/external/{externalId}`).\n- `rate_archived` — `rateId` or `rateExternalId` names an archived rate, which cannot be newly assigned. A record already carrying it may keep it. Choose an active rate (`GET /rates?archived=false`).","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"customer_not_found","message":"`customerId` or `customerExternalId` names no customer of this organization."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"The per-key budget for this window is spent. Failed-authentication throttling carries no per-key `X-RateLimit-*` headers. Wait for `Retry-After`, a delta in seconds, then retry.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"Retry-After":{"description":"Minimum delay before retrying, in seconds (a delta, not Unix epoch seconds).","required":true,"schema":{"type":"integer","minimum":1,"example":30}}},"content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"An unexpected error, with a `requestId` for support. Quote the `requestId` (or `X-Request-Id`) in a support request.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"internal_error","message":"An unexpected error occurred. Quote the request ID when contacting support.","requestId":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/integrations/v1/projects/external/{externalId}":{"get":{"operationId":"getProjectByExternalId","tags":["Projects"],"summary":"Fetch one project by externalId","description":"Required scope: `masterdata:read`. The project linked to your `externalId`, exactly as `GET /projects/{id}` returns it and with the same `ETag`, which is what `If-Match` on `PUT /projects/external/{externalId}` takes. Deactivated projects are returned too, with `deactivated: true`. An `externalId` no project is linked to returns `404`.","x-required-scopes":["masterdata:read"],"x-badges":[{"name":"masterdata:read","position":"before"}],"x-scalar-stability":"stable","parameters":[{"schema":{"type":"string","pattern":"^[\\x21-\\x7e](?:[\\x20-\\x7e]{0,126}[\\x21-\\x7e])?$","example":"K-1001","description":"Your system's identifier for this record: 1–128 printable ASCII characters, no leading or trailing spaces, case-sensitive. Unique per organization and resource; the upsert addresses records by it."},"required":true,"description":"Your system's identifier for this record: 1–128 printable ASCII characters, no leading or trailing spaces, case-sensitive. Unique per organization and resource; the upsert addresses records by it. Percent-encode it in the path.","name":"externalId","in":"path"}],"responses":{"200":{"description":"The project.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"ETag":{"description":"Strong validator over this exact body. Send it back as `If-None-Match` on the next poll and an unchanged page answers `304` with no body. It covers the whole request — filters, cursor, and whether this key may see money — so tags from two different keys or two different queries never match.","required":true,"schema":{"type":"string","example":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\""}},"Cache-Control":{"description":"`no-cache` means revalidate before reuse, not do not store — it is what makes a conditional request happen. `private` keeps a body scoped to one API key out of shared caches.","required":true,"schema":{"type":"string","example":"private, no-cache"}},"Vary":{"description":"The response depends on the key that asked: two keys for the same organization can see different amounts on the same URL.","required":true,"schema":{"type":"string","example":"Authorization"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Project"}}}},"304":{"description":"The representation is unchanged since the `ETag` in `If-None-Match`. No body. This still counts against the rate limit — the server ran the query and built the response; only the transfer was skipped.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"ETag":{"description":"Strong validator over this exact body. Send it back as `If-None-Match` on the next poll and an unchanged page answers `304` with no body. It covers the whole request — filters, cursor, and whether this key may see money — so tags from two different keys or two different queries never match.","required":true,"schema":{"type":"string","example":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\""}}}},"400":{"description":"The request was rejected before it ran. `error.code` says why:\n\n- `invalid_parameters` — a query, path or body value does not match the schema. Fix the fields `details[]` names.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"invalid_parameters","message":"Invalid request parameters.","details":[{"field":"limit","code":"too_big","message":"`limit` must be at most 200."}]}},"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"401":{"description":"The key is missing, unknown, revoked or expired, or the organization has switched the Integrations API off, and the response deliberately does not say which. Send a valid key as `Authorization: Bearer sk_tf_int_live_…`. If a key that worked before answers `401`, have an administrator check that it was not revoked or expired and that the API is still switched on.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"WWW-Authenticate":{"description":"Bearer authentication challenge returned on every 401.","required":true,"schema":{"type":"string","example":"Bearer"}}},"content":{"application/json":{"example":{"error":{"code":"unauthorized","message":"Missing or invalid API key. Send it as `Authorization: Bearer sk_tf_int_live_…`."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key is missing the `masterdata:read` scope this endpoint requires. Scopes are fixed at key creation, so issue a new key carrying the scope named in the message.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"forbidden_scope","message":"This API key is missing the required scope `masterdata:read`. Scopes are fixed at key creation — issue a new key to change them."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Project was not found.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"not_found","message":"Project was not found."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"The per-key budget for this window is spent. Failed-authentication throttling carries no per-key `X-RateLimit-*` headers. Wait for `Retry-After`, a delta in seconds, then retry.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"Retry-After":{"description":"Minimum delay before retrying, in seconds (a delta, not Unix epoch seconds).","required":true,"schema":{"type":"integer","minimum":1,"example":30}}},"content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"An unexpected error, with a `requestId` for support. Quote the `requestId` (or `X-Request-Id`) in a support request.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"internal_error","message":"An unexpected error occurred. Quote the request ID when contacting support.","requestId":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"schema":{"$ref":"#/components/schemas/Error"}}}}}},"put":{"operationId":"upsertProjectByExternalId","tags":["Projects"],"summary":"Upsert a project by externalId","description":"Required scope: `projects:write`. The sync write: updates the project linked to `externalId`, or creates one linked to it. `201` when it created, `200` otherwise. Safe to send again: an unchanged body writes nothing, and two requests racing to create the same `externalId` create one project. `templateProjectId` applies only when this request creates.\n\n**Adopting what was typed in by hand.** When no project is linked yet and `adoptExisting=true` is sent with a `projectNumber`, the one unlinked project carrying that number is linked instead, and the body applied to it (`200`). More than one is `409` `ambiguous_adoption`, listing them in `details[]`; none creates. A project already linked to another `externalId` is never adopted.\n\n**Preconditions.** `If-None-Match: *` creates only, `If-Match: *` updates only, and `If-Match` with an `ETag` updates only the representation you saw; each answers `412` `precondition_failed` otherwise and writes nothing.","x-required-scopes":["projects:write"],"x-badges":[{"name":"projects:write","position":"before"}],"x-scalar-stability":"stable","parameters":[{"schema":{"type":"string","pattern":"^[\\x21-\\x7e](?:[\\x20-\\x7e]{0,126}[\\x21-\\x7e])?$","example":"K-1001","description":"Your system's identifier for this record: 1–128 printable ASCII characters, no leading or trailing spaces, case-sensitive. Unique per organization and resource; the upsert addresses records by it."},"required":true,"description":"Your system's identifier for this record: 1–128 printable ASCII characters, no leading or trailing spaces, case-sensitive. Unique per organization and resource; the upsert addresses records by it. Percent-encode it in the path.","name":"externalId","in":"path"},{"schema":{"type":"string","enum":["true","false"],"description":"When no record is linked to the `externalId` yet, link an existing unlinked one instead of creating a new one, then apply the body to it. Off by default. See the operation description for how a match is found."},"required":false,"description":"When no record is linked to the `externalId` yet, link an existing unlinked one instead of creating a new one, then apply the body to it. Off by default. See the operation description for how a match is found.","name":"adoptExisting","in":"query"},{"schema":{"type":"string","pattern":"^(?:\\*|(?:W\\/)?\"[\\x21\\x23-\\x7e]*\")$","description":"Optional: the `ETag` of the record as you last read or wrote it. The write runs only if the record still has exactly that representation for this key, and answers `412` `precondition_failed` otherwise, writing nothing. The tag covers the whole body, so renaming what the record names (its rate, its customer, the person or key in `createdBy` or `updatedBy`) also fails it without anyone writing the record: re-read and send again. Without it the last write wins. A weak tag (`W/\"…\"`, as a compressed response carries it) counts as the strong one it was; a list or anything else malformed is `400`. `*` means update only: `412` when no record is linked to the `externalId`. A tag also fails with `412` when none is linked.","example":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\""},"required":false,"description":"Optional: the `ETag` of the record as you last read or wrote it. The write runs only if the record still has exactly that representation for this key, and answers `412` `precondition_failed` otherwise, writing nothing. The tag covers the whole body, so renaming what the record names (its rate, its customer, the person or key in `createdBy` or `updatedBy`) also fails it without anyone writing the record: re-read and send again. Without it the last write wins. A weak tag (`W/\"…\"`, as a compressed response carries it) counts as the strong one it was; a list or anything else malformed is `400`. `*` means update only: `412` when no record is linked to the `externalId`. A tag also fails with `412` when none is linked.","name":"If-Match","in":"header"},{"schema":{"type":"string","pattern":"^\\*$","description":"Optional, and only `*`: create only. `412` `precondition_failed` when a record is already linked to the `externalId`. Adopting an unlinked record with `adoptExisting` still happens, since it was not linked.","example":"*"},"required":false,"description":"Optional, and only `*`: create only. `412` `precondition_failed` when a record is already linked to the `externalId`. Adopting an unlinked record with `adoptExisting` still happens, since it was not linked.","name":"If-None-Match","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectUpsertRequest"}}}},"responses":{"200":{"description":"The project linked to `externalId` (or adopted for it), as it now is, whether or not anything changed.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"ETag":{"description":"Strong validator of the record as `GET /{id}` returns it for this key. Send it as `If-Match` on the next write to make that write conditional.","required":true,"schema":{"type":"string","example":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\""}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Project"}}}},"201":{"description":"No project was linked to `externalId`; this one was created for it.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"ETag":{"description":"Strong validator of the record as `GET /{id}` returns it for this key. Send it as `If-Match` on the next write to make that write conditional.","required":true,"schema":{"type":"string","example":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\""}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Project"}}}},"400":{"description":"The request was rejected before it ran. `error.code` says why:\n\n- `malformed_json` — the request body is not valid JSON. Check the payload and the `Content-Type` header.\n- `invalid_parameters` — a query, path or body value does not match the schema. Fix the fields `details[]` names.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"invalid_parameters","message":"Invalid request parameters.","details":[{"field":"rateId","code":"invalid_format","message":"`rateId` must be a UUID."}]}},"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"401":{"description":"The key is missing, unknown, revoked or expired, or the organization has switched the Integrations API off, and the response deliberately does not say which. Send a valid key as `Authorization: Bearer sk_tf_int_live_…`. If a key that worked before answers `401`, have an administrator check that it was not revoked or expired and that the API is still switched on.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"WWW-Authenticate":{"description":"Bearer authentication challenge returned on every 401.","required":true,"schema":{"type":"string","example":"Bearer"}}},"content":{"application/json":{"example":{"error":{"code":"unauthorized","message":"Missing or invalid API key. Send it as `Authorization: Bearer sk_tf_int_live_…`."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key is missing the `projects:write` scope this endpoint requires. Scopes are fixed at key creation, so issue a new key carrying the scope named in the message. Also returned as `money_unavailable` when the body sends `rateId` or `rateExternalId` while the key may not see money: without `money:read`, or with the organization's accounting feature switched off. `details[]` says which.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"forbidden_scope","message":"This API key is missing the required scope `projects:write`. Scopes are fixed at key creation — issue a new key to change them."}},"schema":{"anyOf":[{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/MoneyUnavailableError"}]}}}},"409":{"description":"The write was refused, so nothing was written. `error.code` says why:\n\n- `ambiguous_adoption` — `adoptExisting` found more than one unlinked record to adopt: projects with the body's `projectNumber`, or customers or hourly rates with the body's name, compared case-insensitively. `details[]` lists them. Nothing was written. Link the right record explicitly with `PATCH /projects/{id}`, `PATCH /customers/{id}` or `PATCH /rates/{id}` and its `externalId`.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"ambiguous_adoption","message":"More than one unlinked project carries this `projectNumber`, so none was adopted. Link the right one with `PATCH /projects/{id}`: the candidates are in `details[]`.","details":[{"id":"77777777-7777-4777-8777-777777777777","code":"adoption_candidate"},{"id":"77777777-7777-4777-8777-777777777778","code":"adoption_candidate"}]}},"schema":{"$ref":"#/components/schemas/Error"}}}},"412":{"description":"`If-Match` (a batch item's `ifMatch`) names a representation the record no longer has, or `If-Match: *` / `If-None-Match: *` disagree with whether the `externalId` is linked. Nothing was written. On a stale tag, re-read the record, reconcile the change, and send the write again with the new `ETag`. On `If-None-Match: *` the `externalId` is already linked, and on `If-Match: *` it is not: decide whether to update or create, and resend without the header.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"precondition_failed","message":"The request's `If-Match` or `If-None-Match` precondition does not hold, so nothing was written. Re-read the record and decide again."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"The request is well-formed but names something this organization cannot use here, so nothing was written. `error.code` says why:\n\n- `customer_not_found` — `customerId` or `customerExternalId` names no customer of this organization. Check the reference; when the customer is new, create it first.\n- `template_not_found` — `templateProjectId` names no project of this organization. Check the id, or create the project without a template.\n- `rate_not_found` — `rateId` or `rateExternalId` names no hourly rate of this organization. Check the reference against `GET /rates`; when the rate is new, create it first (`PUT /rates/external/{externalId}`).\n- `rate_archived` — `rateId` or `rateExternalId` names an archived rate, which cannot be newly assigned. A record already carrying it may keep it. Choose an active rate (`GET /rates?archived=false`).","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"customer_not_found","message":"`customerId` or `customerExternalId` names no customer of this organization."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"The per-key budget for this window is spent. Failed-authentication throttling carries no per-key `X-RateLimit-*` headers. Wait for `Retry-After`, a delta in seconds, then retry.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"Retry-After":{"description":"Minimum delay before retrying, in seconds (a delta, not Unix epoch seconds).","required":true,"schema":{"type":"integer","minimum":1,"example":30}}},"content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"An unexpected error, with a `requestId` for support. Quote the `requestId` (or `X-Request-Id`) in a support request.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"internal_error","message":"An unexpected error occurred. Quote the request ID when contacting support.","requestId":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/integrations/v1/projects/{id}/hours":{"get":{"operationId":"getProjectHours","tags":["Projects"],"summary":"Fetch a project's hours","description":"Required scope: `bookings:read`. Hours booked on a project over its whole life, split by billing state, next to its budget (`plannedHours`). Summed live from the bookings, so it is current when it answers. An unknown id returns `404`.","x-required-scopes":["bookings:read"],"x-badges":[{"name":"bookings:read","position":"before"}],"x-scalar-stability":"stable","parameters":[{"schema":{"type":"string","format":"uuid","description":"Stable identifier of the project to retrieve."},"required":true,"description":"Stable identifier of the project to retrieve.","name":"id","in":"path"}],"responses":{"200":{"description":"The project's hours.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"ETag":{"description":"Strong validator over this exact body. Send it back as `If-None-Match` on the next poll and an unchanged page answers `304` with no body. It covers the whole request — filters, cursor, and whether this key may see money — so tags from two different keys or two different queries never match.","required":true,"schema":{"type":"string","example":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\""}},"Cache-Control":{"description":"`no-cache` means revalidate before reuse, not do not store — it is what makes a conditional request happen. `private` keeps a body scoped to one API key out of shared caches.","required":true,"schema":{"type":"string","example":"private, no-cache"}},"Vary":{"description":"The response depends on the key that asked: two keys for the same organization can see different amounts on the same URL.","required":true,"schema":{"type":"string","example":"Authorization"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectHours"}}}},"304":{"description":"The representation is unchanged since the `ETag` in `If-None-Match`. No body. This still counts against the rate limit — the server ran the query and built the response; only the transfer was skipped.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"ETag":{"description":"Strong validator over this exact body. Send it back as `If-None-Match` on the next poll and an unchanged page answers `304` with no body. It covers the whole request — filters, cursor, and whether this key may see money — so tags from two different keys or two different queries never match.","required":true,"schema":{"type":"string","example":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\""}}}},"400":{"description":"The request was rejected before it ran. `error.code` says why:\n\n- `invalid_parameters` — a query, path or body value does not match the schema. Fix the fields `details[]` names.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"invalid_parameters","message":"Invalid request parameters.","details":[{"field":"limit","code":"too_big","message":"`limit` must be at most 200."}]}},"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"401":{"description":"The key is missing, unknown, revoked or expired, or the organization has switched the Integrations API off, and the response deliberately does not say which. Send a valid key as `Authorization: Bearer sk_tf_int_live_…`. If a key that worked before answers `401`, have an administrator check that it was not revoked or expired and that the API is still switched on.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"WWW-Authenticate":{"description":"Bearer authentication challenge returned on every 401.","required":true,"schema":{"type":"string","example":"Bearer"}}},"content":{"application/json":{"example":{"error":{"code":"unauthorized","message":"Missing or invalid API key. Send it as `Authorization: Bearer sk_tf_int_live_…`."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key is missing the `bookings:read` scope this endpoint requires. Scopes are fixed at key creation, so issue a new key carrying the scope named in the message.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"forbidden_scope","message":"This API key is missing the required scope `bookings:read`. Scopes are fixed at key creation — issue a new key to change them."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Project was not found.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"not_found","message":"Project was not found."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"The per-key budget for this window is spent. Failed-authentication throttling carries no per-key `X-RateLimit-*` headers. Wait for `Retry-After`, a delta in seconds, then retry.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"Retry-After":{"description":"Minimum delay before retrying, in seconds (a delta, not Unix epoch seconds).","required":true,"schema":{"type":"integer","minimum":1,"example":30}}},"content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"An unexpected error, with a `requestId` for support. Quote the `requestId` (or `X-Request-Id`) in a support request.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"internal_error","message":"An unexpected error occurred. Quote the request ID when contacting support.","requestId":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/integrations/v1/tasks":{"get":{"operationId":"listTasks","tags":["Tasks"],"summary":"List tasks","description":"Required scope: `masterdata:read`. Oldest first by creation, in pages walked with `cursor`. `?sort=updatedAt` walks by last modification instead, with `from`/`to` bounding that walk. Each task carries `parentTaskId` for rebuilding the tree and its own resolved `path`.","x-required-scopes":["masterdata:read"],"x-badges":[{"name":"masterdata:read","position":"before"}],"x-scalar-stability":"stable","parameters":[{"schema":{"type":"array","items":{"type":"string","format":"uuid"},"minItems":1,"maxItems":100,"description":"Only the tasks named here, by TimeFusion's `id`: the list form of `GET /tasks/{id}`, for resolving many at once, such as every `taskId` on a page of bookings. An unknown id contributes no rows rather than an error. Repeat the parameter for several values — `?id=a&id=b` — and a task matches any of them; at most 100 per request. Repeated values within one parameter are ORed, different parameters are ANDed, and duplicates are ignored. Bracketed keys (`id[]`, `id[0]`) are rejected with `400`.","example":["11111111-1111-4111-8111-111111111111","22222222-2222-4222-8222-222222222222"]},"required":false,"description":"Only the tasks named here, by TimeFusion's `id`: the list form of `GET /tasks/{id}`, for resolving many at once, such as every `taskId` on a page of bookings. An unknown id contributes no rows rather than an error. Repeat the parameter for several values — `?id=a&id=b` — and a task matches any of them; at most 100 per request. Repeated values within one parameter are ORed, different parameters are ANDed, and duplicates are ignored. Bracketed keys (`id[]`, `id[0]`) are rejected with `400`.","name":"id","in":"query","style":"form","explode":true},{"schema":{"type":"array","items":{"type":"string","format":"uuid"},"minItems":1,"maxItems":50,"description":"Only tasks of this project. Repeat the parameter for several values — `?projectId=a&projectId=b` — and a task matches any of them; at most 50 per request. Repeated values within one parameter are ORed, different parameters are ANDed, and duplicates are ignored. Bracketed keys (`projectId[]`, `projectId[0]`) are rejected with `400`.","example":["11111111-1111-4111-8111-111111111111","22222222-2222-4222-8222-222222222222"]},"required":false,"description":"Only tasks of this project. Repeat the parameter for several values — `?projectId=a&projectId=b` — and a task matches any of them; at most 50 per request. Repeated values within one parameter are ORed, different parameters are ANDed, and duplicates are ignored. Bracketed keys (`projectId[]`, `projectId[0]`) are rejected with `400`.","name":"projectId","in":"query","style":"form","explode":true},{"schema":{"type":"array","items":{"type":"string","minLength":1},"minItems":1,"maxItems":50,"description":"Only tasks at these levels of the tree, matched against the `kind` a task publishes: `subArea`, `serviceArea` or `activity` today. An open vocabulary: a value this organization does not use matches nothing rather than failing the request. A flat task carries `kind: null` and no value here selects it; omit the filter and read `kind` per task. Repeat the parameter for several values — `?kind=subArea&kind=activity` — and a task matches any of them; at most 50 per request. Repeated values within one parameter are ORed, different parameters are ANDed, and duplicates are ignored. Bracketed keys (`kind[]`, `kind[0]`) are rejected with `400`.","example":["subArea","activity"]},"required":false,"description":"Only tasks at these levels of the tree, matched against the `kind` a task publishes: `subArea`, `serviceArea` or `activity` today. An open vocabulary: a value this organization does not use matches nothing rather than failing the request. A flat task carries `kind: null` and no value here selects it; omit the filter and read `kind` per task. Repeat the parameter for several values — `?kind=subArea&kind=activity` — and a task matches any of them; at most 50 per request. Repeated values within one parameter are ORed, different parameters are ANDed, and duplicates are ignored. Bracketed keys (`kind[]`, `kind[0]`) are rejected with `400`.","name":"kind","in":"query","style":"form","explode":true},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}([T ]\\d{2}:\\d{2}(:\\d{2}(\\.\\d{1,3})?)?(Z|[+-]\\d{2}:\\d{2})?)?$","description":"Inclusive start of the walk. **Only valid with `sort=updatedAt`**, where it selects rows last written at or after this instant; rejected as `400 invalid_parameters` with the default `sort=createdAt`. A date without a time means 00:00:00.000 UTC on that day. Timestamps without an offset are interpreted as UTC; explicit offsets are respected.","example":"2026-08-01"},"required":false,"description":"Inclusive start of the walk. **Only valid with `sort=updatedAt`**, where it selects rows last written at or after this instant; rejected as `400 invalid_parameters` with the default `sort=createdAt`. A date without a time means 00:00:00.000 UTC on that day. Timestamps without an offset are interpreted as UTC; explicit offsets are respected.","name":"from","in":"query"},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}([T ]\\d{2}:\\d{2}(:\\d{2}(\\.\\d{1,3})?)?(Z|[+-]\\d{2}:\\d{2})?)?$","description":"Inclusive end of the walk. **Only valid with `sort=updatedAt`**, where it selects rows last written at or before this instant; rejected as `400 invalid_parameters` with the default `sort=createdAt`. A date without a time means 23:59:59.999 UTC on that day. Timestamps without an offset are interpreted as UTC; explicit offsets are respected.","example":"2026-08-31"},"required":false,"description":"Inclusive end of the walk. **Only valid with `sort=updatedAt`**, where it selects rows last written at or before this instant; rejected as `400 invalid_parameters` with the default `sort=createdAt`. A date without a time means 23:59:59.999 UTC on that day. Timestamps without an offset are interpreted as UTC; explicit offsets are respected.","name":"to","in":"query"},{"schema":{"type":"string","enum":["createdAt","updatedAt"],"default":"createdAt","description":"Which order to walk, and which timestamp `from`/`to` bound. `createdAt` (default) walks oldest-created first. `updatedAt` walks by last modification, oldest first — the ordering a mirror polls to find which rows changed. `from` and `to` are only accepted with `updatedAt`. Cursors are not interchangeable between the two.","example":"createdAt"},"required":false,"description":"Which order to walk, and which timestamp `from`/`to` bound. `createdAt` (default) walks oldest-created first. `updatedAt` walks by last modification, oldest first — the ordering a mirror polls to find which rows changed. `from` and `to` are only accepted with `updatedAt`. Cursors are not interchangeable between the two.","name":"sort","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":200,"default":50,"description":"Page size, 1–200. Values above 200 are rejected, not clamped.","example":50},"required":false,"description":"Page size, 1–200. Values above 200 are rejected, not clamped.","name":"limit","in":"query"},{"schema":{"type":"string","description":"Opaque cursor from a previous page's `pagination.nextCursor` **on this endpoint**. Cursors are not interchangeable between endpoints; one issued elsewhere is rejected as `invalid_cursor`. Omit for the first page."},"required":false,"description":"Opaque cursor from a previous page's `pagination.nextCursor` **on this endpoint**. Cursors are not interchangeable between endpoints; one issued elsewhere is rejected as `invalid_cursor`. Omit for the first page.","name":"cursor","in":"query"}],"responses":{"200":{"description":"One page of tasks.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"ETag":{"description":"Strong validator over this exact body. Send it back as `If-None-Match` on the next poll and an unchanged page answers `304` with no body. It covers the whole request — filters, cursor, and whether this key may see money — so tags from two different keys or two different queries never match.","required":true,"schema":{"type":"string","example":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\""}},"Cache-Control":{"description":"`no-cache` means revalidate before reuse, not do not store — it is what makes a conditional request happen. `private` keeps a body scoped to one API key out of shared caches.","required":true,"schema":{"type":"string","example":"private, no-cache"}},"Vary":{"description":"The response depends on the key that asked: two keys for the same organization can see different amounts on the same URL.","required":true,"schema":{"type":"string","example":"Authorization"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaskPage"}}}},"304":{"description":"The representation is unchanged since the `ETag` in `If-None-Match`. No body. This still counts against the rate limit — the server ran the query and built the response; only the transfer was skipped.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"ETag":{"description":"Strong validator over this exact body. Send it back as `If-None-Match` on the next poll and an unchanged page answers `304` with no body. It covers the whole request — filters, cursor, and whether this key may see money — so tags from two different keys or two different queries never match.","required":true,"schema":{"type":"string","example":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\""}}}},"400":{"description":"The request was rejected before it ran. `error.code` says why:\n\n- `invalid_parameters` — a query, path or body value does not match the schema. Fix the fields `details[]` names.\n- `invalid_period` — `from` or `to` is date-shaped but not a real date (`2026-13-01`, `2026-02-31`), or `to` is earlier than `from`. A boundary that is not date-shaped at all fails the schema as `invalid_parameters` instead. Send a real calendar date, with `to` no earlier than `from`.\n- `invalid_cursor` — the `cursor` was not issued by this API. Discard the cursor and restart the walk from the first page.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"invalid_parameters","message":"Invalid request parameters.","details":[{"field":"limit","code":"too_big","message":"`limit` must be at most 200."}]}},"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"401":{"description":"The key is missing, unknown, revoked or expired, or the organization has switched the Integrations API off, and the response deliberately does not say which. Send a valid key as `Authorization: Bearer sk_tf_int_live_…`. If a key that worked before answers `401`, have an administrator check that it was not revoked or expired and that the API is still switched on.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"WWW-Authenticate":{"description":"Bearer authentication challenge returned on every 401.","required":true,"schema":{"type":"string","example":"Bearer"}}},"content":{"application/json":{"example":{"error":{"code":"unauthorized","message":"Missing or invalid API key. Send it as `Authorization: Bearer sk_tf_int_live_…`."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key is missing the `masterdata:read` scope this endpoint requires. Scopes are fixed at key creation, so issue a new key carrying the scope named in the message.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"forbidden_scope","message":"This API key is missing the required scope `masterdata:read`. Scopes are fixed at key creation — issue a new key to change them."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"The per-key budget for this window is spent. Failed-authentication throttling carries no per-key `X-RateLimit-*` headers. Wait for `Retry-After`, a delta in seconds, then retry.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"Retry-After":{"description":"Minimum delay before retrying, in seconds (a delta, not Unix epoch seconds).","required":true,"schema":{"type":"integer","minimum":1,"example":30}}},"content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"An unexpected error, with a `requestId` for support. Quote the `requestId` (or `X-Request-Id`) in a support request.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"internal_error","message":"An unexpected error occurred. Quote the request ID when contacting support.","requestId":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/integrations/v1/tasks/{id}":{"get":{"operationId":"getTask","tags":["Tasks"],"summary":"Fetch one task by id","description":"Required scope: `masterdata:read`. A single task by id. An unknown id returns `404`.","x-required-scopes":["masterdata:read"],"x-badges":[{"name":"masterdata:read","position":"before"}],"x-scalar-stability":"stable","parameters":[{"schema":{"type":"string","format":"uuid","description":"Stable identifier of the task to retrieve."},"required":true,"description":"Stable identifier of the task to retrieve.","name":"id","in":"path"}],"responses":{"200":{"description":"The task.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"ETag":{"description":"Strong validator over this exact body. Send it back as `If-None-Match` on the next poll and an unchanged page answers `304` with no body. It covers the whole request — filters, cursor, and whether this key may see money — so tags from two different keys or two different queries never match.","required":true,"schema":{"type":"string","example":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\""}},"Cache-Control":{"description":"`no-cache` means revalidate before reuse, not do not store — it is what makes a conditional request happen. `private` keeps a body scoped to one API key out of shared caches.","required":true,"schema":{"type":"string","example":"private, no-cache"}},"Vary":{"description":"The response depends on the key that asked: two keys for the same organization can see different amounts on the same URL.","required":true,"schema":{"type":"string","example":"Authorization"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Task"}}}},"304":{"description":"The representation is unchanged since the `ETag` in `If-None-Match`. No body. This still counts against the rate limit — the server ran the query and built the response; only the transfer was skipped.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"ETag":{"description":"Strong validator over this exact body. Send it back as `If-None-Match` on the next poll and an unchanged page answers `304` with no body. It covers the whole request — filters, cursor, and whether this key may see money — so tags from two different keys or two different queries never match.","required":true,"schema":{"type":"string","example":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\""}}}},"400":{"description":"The request was rejected before it ran. `error.code` says why:\n\n- `invalid_parameters` — a query, path or body value does not match the schema. Fix the fields `details[]` names.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"invalid_parameters","message":"Invalid request parameters.","details":[{"field":"limit","code":"too_big","message":"`limit` must be at most 200."}]}},"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"401":{"description":"The key is missing, unknown, revoked or expired, or the organization has switched the Integrations API off, and the response deliberately does not say which. Send a valid key as `Authorization: Bearer sk_tf_int_live_…`. If a key that worked before answers `401`, have an administrator check that it was not revoked or expired and that the API is still switched on.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"WWW-Authenticate":{"description":"Bearer authentication challenge returned on every 401.","required":true,"schema":{"type":"string","example":"Bearer"}}},"content":{"application/json":{"example":{"error":{"code":"unauthorized","message":"Missing or invalid API key. Send it as `Authorization: Bearer sk_tf_int_live_…`."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key is missing the `masterdata:read` scope this endpoint requires. Scopes are fixed at key creation, so issue a new key carrying the scope named in the message.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"forbidden_scope","message":"This API key is missing the required scope `masterdata:read`. Scopes are fixed at key creation — issue a new key to change them."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Task was not found.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"not_found","message":"Task was not found."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"The per-key budget for this window is spent. Failed-authentication throttling carries no per-key `X-RateLimit-*` headers. Wait for `Retry-After`, a delta in seconds, then retry.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"Retry-After":{"description":"Minimum delay before retrying, in seconds (a delta, not Unix epoch seconds).","required":true,"schema":{"type":"integer","minimum":1,"example":30}}},"content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"An unexpected error, with a `requestId` for support. Quote the `requestId` (or `X-Request-Id`) in a support request.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"internal_error","message":"An unexpected error occurred. Quote the request ID when contacting support.","requestId":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/integrations/v1/customers":{"get":{"operationId":"listCustomers","tags":["Customers"],"summary":"List customers","description":"Required scope: `masterdata:read`. Oldest first by creation, in pages walked with `cursor`. `?sort=updatedAt` walks by last modification instead, with `from`/`to` bounding that walk. Archived customers are returned and flagged, not hidden.","x-required-scopes":["masterdata:read"],"x-badges":[{"name":"masterdata:read","position":"before"}],"x-scalar-stability":"stable","parameters":[{"schema":{"type":"array","items":{"type":"string","format":"uuid"},"minItems":1,"maxItems":100,"description":"Only the customers named here, by TimeFusion's `id`: the list form of `GET /customers/{id}`, for resolving many at once. An unknown id contributes no rows rather than an error. Repeat the parameter for several values — `?id=a&id=b` — and a customer matches any of them; at most 100 per request. Repeated values within one parameter are ORed, different parameters are ANDed, and duplicates are ignored. Bracketed keys (`id[]`, `id[0]`) are rejected with `400`.","example":["11111111-1111-4111-8111-111111111111","22222222-2222-4222-8222-222222222222"]},"required":false,"description":"Only the customers named here, by TimeFusion's `id`: the list form of `GET /customers/{id}`, for resolving many at once. An unknown id contributes no rows rather than an error. Repeat the parameter for several values — `?id=a&id=b` — and a customer matches any of them; at most 100 per request. Repeated values within one parameter are ORed, different parameters are ANDed, and duplicates are ignored. Bracketed keys (`id[]`, `id[0]`) are rejected with `400`.","name":"id","in":"query","style":"form","explode":true},{"schema":{"type":"array","items":{"type":"string","pattern":"^[\\x21-\\x7e](?:[\\x20-\\x7e]{0,126}[\\x21-\\x7e])?$"},"minItems":1,"maxItems":50,"description":"Only customers linked to this `externalId`, matched exactly (case-sensitive). Repeat the parameter for several values — `?externalId=K-1001&externalId=K-1002` — and a customer matches any of them; at most 50 per request. Repeated values within one parameter are ORed, different parameters are ANDed, and duplicates are ignored. Bracketed keys (`externalId[]`, `externalId[0]`) are rejected with `400`.","example":["K-1001","K-1002"]},"required":false,"description":"Only customers linked to this `externalId`, matched exactly (case-sensitive). Repeat the parameter for several values — `?externalId=K-1001&externalId=K-1002` — and a customer matches any of them; at most 50 per request. Repeated values within one parameter are ORed, different parameters are ANDed, and duplicates are ignored. Bracketed keys (`externalId[]`, `externalId[0]`) are rejected with `400`.","name":"externalId","in":"query","style":"form","explode":true},{"schema":{"type":"string","enum":["true","false"],"description":"Filter by the archived flag. Omit to receive both."},"required":false,"description":"Filter by the archived flag. Omit to receive both.","name":"archived","in":"query"},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}([T ]\\d{2}:\\d{2}(:\\d{2}(\\.\\d{1,3})?)?(Z|[+-]\\d{2}:\\d{2})?)?$","description":"Inclusive start of the walk. **Only valid with `sort=updatedAt`**, where it selects rows last written at or after this instant; rejected as `400 invalid_parameters` with the default `sort=createdAt`. A date without a time means 00:00:00.000 UTC on that day. Timestamps without an offset are interpreted as UTC; explicit offsets are respected.","example":"2026-08-01"},"required":false,"description":"Inclusive start of the walk. **Only valid with `sort=updatedAt`**, where it selects rows last written at or after this instant; rejected as `400 invalid_parameters` with the default `sort=createdAt`. A date without a time means 00:00:00.000 UTC on that day. Timestamps without an offset are interpreted as UTC; explicit offsets are respected.","name":"from","in":"query"},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}([T ]\\d{2}:\\d{2}(:\\d{2}(\\.\\d{1,3})?)?(Z|[+-]\\d{2}:\\d{2})?)?$","description":"Inclusive end of the walk. **Only valid with `sort=updatedAt`**, where it selects rows last written at or before this instant; rejected as `400 invalid_parameters` with the default `sort=createdAt`. A date without a time means 23:59:59.999 UTC on that day. Timestamps without an offset are interpreted as UTC; explicit offsets are respected.","example":"2026-08-31"},"required":false,"description":"Inclusive end of the walk. **Only valid with `sort=updatedAt`**, where it selects rows last written at or before this instant; rejected as `400 invalid_parameters` with the default `sort=createdAt`. A date without a time means 23:59:59.999 UTC on that day. Timestamps without an offset are interpreted as UTC; explicit offsets are respected.","name":"to","in":"query"},{"schema":{"type":"string","enum":["createdAt","updatedAt"],"default":"createdAt","description":"Which order to walk, and which timestamp `from`/`to` bound. `createdAt` (default) walks oldest-created first. `updatedAt` walks by last modification, oldest first — the ordering a mirror polls to find which rows changed. `from` and `to` are only accepted with `updatedAt`. Cursors are not interchangeable between the two.","example":"createdAt"},"required":false,"description":"Which order to walk, and which timestamp `from`/`to` bound. `createdAt` (default) walks oldest-created first. `updatedAt` walks by last modification, oldest first — the ordering a mirror polls to find which rows changed. `from` and `to` are only accepted with `updatedAt`. Cursors are not interchangeable between the two.","name":"sort","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":200,"default":50,"description":"Page size, 1–200. Values above 200 are rejected, not clamped.","example":50},"required":false,"description":"Page size, 1–200. Values above 200 are rejected, not clamped.","name":"limit","in":"query"},{"schema":{"type":"string","description":"Opaque cursor from a previous page's `pagination.nextCursor` **on this endpoint**. Cursors are not interchangeable between endpoints; one issued elsewhere is rejected as `invalid_cursor`. Omit for the first page."},"required":false,"description":"Opaque cursor from a previous page's `pagination.nextCursor` **on this endpoint**. Cursors are not interchangeable between endpoints; one issued elsewhere is rejected as `invalid_cursor`. Omit for the first page.","name":"cursor","in":"query"}],"responses":{"200":{"description":"One page of customers.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"ETag":{"description":"Strong validator over this exact body. Send it back as `If-None-Match` on the next poll and an unchanged page answers `304` with no body. It covers the whole request — filters, cursor, and whether this key may see money — so tags from two different keys or two different queries never match.","required":true,"schema":{"type":"string","example":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\""}},"Cache-Control":{"description":"`no-cache` means revalidate before reuse, not do not store — it is what makes a conditional request happen. `private` keeps a body scoped to one API key out of shared caches.","required":true,"schema":{"type":"string","example":"private, no-cache"}},"Vary":{"description":"The response depends on the key that asked: two keys for the same organization can see different amounts on the same URL.","required":true,"schema":{"type":"string","example":"Authorization"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerPage"}}}},"304":{"description":"The representation is unchanged since the `ETag` in `If-None-Match`. No body. This still counts against the rate limit — the server ran the query and built the response; only the transfer was skipped.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"ETag":{"description":"Strong validator over this exact body. Send it back as `If-None-Match` on the next poll and an unchanged page answers `304` with no body. It covers the whole request — filters, cursor, and whether this key may see money — so tags from two different keys or two different queries never match.","required":true,"schema":{"type":"string","example":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\""}}}},"400":{"description":"The request was rejected before it ran. `error.code` says why:\n\n- `invalid_parameters` — a query, path or body value does not match the schema. Fix the fields `details[]` names.\n- `invalid_period` — `from` or `to` is date-shaped but not a real date (`2026-13-01`, `2026-02-31`), or `to` is earlier than `from`. A boundary that is not date-shaped at all fails the schema as `invalid_parameters` instead. Send a real calendar date, with `to` no earlier than `from`.\n- `invalid_cursor` — the `cursor` was not issued by this API. Discard the cursor and restart the walk from the first page.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"invalid_parameters","message":"Invalid request parameters.","details":[{"field":"limit","code":"too_big","message":"`limit` must be at most 200."}]}},"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"401":{"description":"The key is missing, unknown, revoked or expired, or the organization has switched the Integrations API off, and the response deliberately does not say which. Send a valid key as `Authorization: Bearer sk_tf_int_live_…`. If a key that worked before answers `401`, have an administrator check that it was not revoked or expired and that the API is still switched on.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"WWW-Authenticate":{"description":"Bearer authentication challenge returned on every 401.","required":true,"schema":{"type":"string","example":"Bearer"}}},"content":{"application/json":{"example":{"error":{"code":"unauthorized","message":"Missing or invalid API key. Send it as `Authorization: Bearer sk_tf_int_live_…`."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key is missing the `masterdata:read` scope this endpoint requires. Scopes are fixed at key creation, so issue a new key carrying the scope named in the message.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"forbidden_scope","message":"This API key is missing the required scope `masterdata:read`. Scopes are fixed at key creation — issue a new key to change them."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"The per-key budget for this window is spent. Failed-authentication throttling carries no per-key `X-RateLimit-*` headers. Wait for `Retry-After`, a delta in seconds, then retry.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"Retry-After":{"description":"Minimum delay before retrying, in seconds (a delta, not Unix epoch seconds).","required":true,"schema":{"type":"integer","minimum":1,"example":30}}},"content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"An unexpected error, with a `requestId` for support. Quote the `requestId` (or `X-Request-Id`) in a support request.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"internal_error","message":"An unexpected error occurred. Quote the request ID when contacting support.","requestId":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"createCustomer","tags":["Customers"],"summary":"Create a customer","description":"Required scope: `customers:write`. Creates a customer, attributed to this key, and answers with it as `GET /customers/{id}` returns it, plus its `ETag`. A new customer is active unless `archived` is sent. Send an `Idempotency-Key` to make a retry safe: without one, a second identical request is a second customer, which the name check then refuses. To link the customer to an id of your own system, use `PUT /customers/external/{externalId}` instead, which is safe to send twice without a key.","x-required-scopes":["customers:write"],"x-badges":[{"name":"customers:write","position":"before"}],"x-scalar-stability":"stable","parameters":[{"schema":{"type":"string","pattern":"^[\\x20-\\x7e]{1,128}$","description":"Optional: 1–128 printable ASCII characters, case-sensitive. Send a unique key — a UUID is ideal — to make this write safely retryable: if you never see the response, send the **identical** request with the **same** key and the original result is returned instead of executing again. Keys are scoped to the organization and operation; any currently authorized API key in that organization may replay the result. Receipts are retained for 7 days from execution and describe that original execution, not the records' current state; a key without `money:read` is shown it without rates. A replay carries `Idempotency-Replayed: true`. The same key with a different body is `409 idempotency_key_reused`. Without this header a retry creates a second record.","example":"3f8c1b9e-2d47-4a1e-9a1b-5f6f0f7c2a10"},"required":false,"description":"Optional: 1–128 printable ASCII characters, case-sensitive. Send a unique key — a UUID is ideal — to make this write safely retryable: if you never see the response, send the **identical** request with the **same** key and the original result is returned instead of executing again. Keys are scoped to the organization and operation; any currently authorized API key in that organization may replay the result. Receipts are retained for 7 days from execution and describe that original execution, not the records' current state; a key without `money:read` is shown it without rates. A replay carries `Idempotency-Replayed: true`. The same key with a different body is `409 idempotency_key_reused`. Without this header a retry creates a second record.","name":"Idempotency-Key","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerCreateRequest"}}}},"responses":{"201":{"description":"The customer was created. With `Idempotency-Replayed: true`, this is the retained answer of an earlier execution: the customer as it was then.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"ETag":{"description":"Strong validator of the record as `GET /{id}` returns it for this key. Send it as `If-Match` on the next write to make that write conditional.","required":true,"schema":{"type":"string","example":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\""}},"Idempotency-Replayed":{"description":"Sent as `true` when this response was replayed from a retained receipt rather than executed. Absent on a first execution. The body is the original one, so it describes that execution and not the records' current state.","schema":{"type":"string","example":"true"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Customer"}}}},"400":{"description":"The request was rejected before it ran. `error.code` says why:\n\n- `malformed_json` — the request body is not valid JSON. Check the payload and the `Content-Type` header.\n- `invalid_parameters` — a query, path or body value does not match the schema. Fix the fields `details[]` names.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"invalid_parameters","message":"Invalid request parameters.","details":[{"field":"rateId","code":"invalid_format","message":"`rateId` must be a UUID."}]}},"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"401":{"description":"The key is missing, unknown, revoked or expired, or the organization has switched the Integrations API off, and the response deliberately does not say which. Send a valid key as `Authorization: Bearer sk_tf_int_live_…`. If a key that worked before answers `401`, have an administrator check that it was not revoked or expired and that the API is still switched on.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"WWW-Authenticate":{"description":"Bearer authentication challenge returned on every 401.","required":true,"schema":{"type":"string","example":"Bearer"}}},"content":{"application/json":{"example":{"error":{"code":"unauthorized","message":"Missing or invalid API key. Send it as `Authorization: Bearer sk_tf_int_live_…`."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key is missing the `customers:write` scope this endpoint requires. Scopes are fixed at key creation, so issue a new key carrying the scope named in the message. Also returned as `money_unavailable` when the body sends `rateId` or `rateExternalId` while the key may not see money: without `money:read`, or with the organization's accounting feature switched off. `details[]` says which.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"forbidden_scope","message":"This API key is missing the required scope `customers:write`. Scopes are fixed at key creation — issue a new key to change them."}},"schema":{"anyOf":[{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/MoneyUnavailableError"}]}}}},"409":{"description":"The write was refused, so nothing was written. `error.code` says why:\n\n- `customer_name_taken` — another customer of the organization already has this name, compared case-insensitively; `details[]` carries its id. Nothing was written. When it is the customer you meant, link it: set your `externalId` on it with `PATCH /customers/{id}`, or send the upsert again with `?adoptExisting=true` (`adoptExisting: true` on a batch). When it is a different customer, choose another name.\n- `idempotency_key_reused` — this `Idempotency-Key` is retained against a different request body. Receipts are kept for 7 days. Send the original request unchanged, or a new key for a new intent.\n- `idempotency_request_in_progress` — another request holding this key is still running and did not finish within the bounded wait. **Retry** the identical request after a few seconds; it replays that request's result rather than executing twice.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"customer_name_taken","message":"Another customer of this organization already has this name (compared case-insensitively); its id is in `details[]`. Link that customer if it is the one you meant, or choose another name.","details":[{"id":"88888888-8888-4888-8888-888888888888","code":"existing_customer"}]}},"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"The request is well-formed but names something this organization cannot use here, so nothing was written. `error.code` says why:\n\n- `rate_not_found` — `rateId` or `rateExternalId` names no hourly rate of this organization. Check the reference against `GET /rates`; when the rate is new, create it first (`PUT /rates/external/{externalId}`).\n- `rate_archived` — `rateId` or `rateExternalId` names an archived rate, which cannot be newly assigned. A record already carrying it may keep it. Choose an active rate (`GET /rates?archived=false`).","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"rate_not_found","message":"`rateId` names no hourly rate of this organization."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"The per-key budget for this window is spent. Failed-authentication throttling carries no per-key `X-RateLimit-*` headers. Wait for `Retry-After`, a delta in seconds, then retry.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"Retry-After":{"description":"Minimum delay before retrying, in seconds (a delta, not Unix epoch seconds).","required":true,"schema":{"type":"integer","minimum":1,"example":30}}},"content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"An unexpected error, with a `requestId` for support. Quote the `requestId` (or `X-Request-Id`) in a support request.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"internal_error","message":"An unexpected error occurred. Quote the request ID when contacting support.","requestId":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/integrations/v1/customers/{id}":{"get":{"operationId":"getCustomer","tags":["Customers"],"summary":"Fetch one customer by id","description":"Required scope: `masterdata:read`. A single customer by id. Archived customers are returned too, with `archived: true`; an unknown id returns `404`.","x-required-scopes":["masterdata:read"],"x-badges":[{"name":"masterdata:read","position":"before"}],"x-scalar-stability":"stable","parameters":[{"schema":{"type":"string","format":"uuid","description":"Stable identifier of the customer to retrieve."},"required":true,"description":"Stable identifier of the customer to retrieve.","name":"id","in":"path"}],"responses":{"200":{"description":"The customer.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"ETag":{"description":"Strong validator over this exact body. Send it back as `If-None-Match` on the next poll and an unchanged page answers `304` with no body. It covers the whole request — filters, cursor, and whether this key may see money — so tags from two different keys or two different queries never match.","required":true,"schema":{"type":"string","example":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\""}},"Cache-Control":{"description":"`no-cache` means revalidate before reuse, not do not store — it is what makes a conditional request happen. `private` keeps a body scoped to one API key out of shared caches.","required":true,"schema":{"type":"string","example":"private, no-cache"}},"Vary":{"description":"The response depends on the key that asked: two keys for the same organization can see different amounts on the same URL.","required":true,"schema":{"type":"string","example":"Authorization"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Customer"}}}},"304":{"description":"The representation is unchanged since the `ETag` in `If-None-Match`. No body. This still counts against the rate limit — the server ran the query and built the response; only the transfer was skipped.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"ETag":{"description":"Strong validator over this exact body. Send it back as `If-None-Match` on the next poll and an unchanged page answers `304` with no body. It covers the whole request — filters, cursor, and whether this key may see money — so tags from two different keys or two different queries never match.","required":true,"schema":{"type":"string","example":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\""}}}},"400":{"description":"The request was rejected before it ran. `error.code` says why:\n\n- `invalid_parameters` — a query, path or body value does not match the schema. Fix the fields `details[]` names.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"invalid_parameters","message":"Invalid request parameters.","details":[{"field":"limit","code":"too_big","message":"`limit` must be at most 200."}]}},"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"401":{"description":"The key is missing, unknown, revoked or expired, or the organization has switched the Integrations API off, and the response deliberately does not say which. Send a valid key as `Authorization: Bearer sk_tf_int_live_…`. If a key that worked before answers `401`, have an administrator check that it was not revoked or expired and that the API is still switched on.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"WWW-Authenticate":{"description":"Bearer authentication challenge returned on every 401.","required":true,"schema":{"type":"string","example":"Bearer"}}},"content":{"application/json":{"example":{"error":{"code":"unauthorized","message":"Missing or invalid API key. Send it as `Authorization: Bearer sk_tf_int_live_…`."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key is missing the `masterdata:read` scope this endpoint requires. Scopes are fixed at key creation, so issue a new key carrying the scope named in the message.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"forbidden_scope","message":"This API key is missing the required scope `masterdata:read`. Scopes are fixed at key creation — issue a new key to change them."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Customer was not found.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"not_found","message":"Customer was not found."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"The per-key budget for this window is spent. Failed-authentication throttling carries no per-key `X-RateLimit-*` headers. Wait for `Retry-After`, a delta in seconds, then retry.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"Retry-After":{"description":"Minimum delay before retrying, in seconds (a delta, not Unix epoch seconds).","required":true,"schema":{"type":"integer","minimum":1,"example":30}}},"content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"An unexpected error, with a `requestId` for support. Quote the `requestId` (or `X-Request-Id`) in a support request.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"internal_error","message":"An unexpected error occurred. Quote the request ID when contacting support.","requestId":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"operationId":"updateCustomer","tags":["Customers"],"summary":"Update a customer by id","description":"Required scope: `customers:write`. Changes the fields the body sends and leaves the others as they are; `null` clears a nullable field. A body that changes nothing writes nothing: `updatedAt` and the `ETag` stay put, which is what keeps two systems syncing in both directions from echoing one change back and forth. Otherwise the change is attributed to this key. Archiving and reactivating are the `archived` field. `externalId` links, re-links or (with `null`) unlinks the customer. A `PATCH` takes no entity tag: to refuse the write when the customer changed since you read it, send its `ETag` as `If-Match` to the upsert by `externalId` instead.","x-required-scopes":["customers:write"],"x-badges":[{"name":"customers:write","position":"before"}],"x-scalar-stability":"stable","parameters":[{"schema":{"type":"string","format":"uuid","description":"Stable identifier of the customer to retrieve."},"required":true,"description":"Stable identifier of the customer to retrieve.","name":"id","in":"path"},{"schema":{"type":"string","pattern":"^\\*$","description":"Only `*`, which always holds here. Any entity tag is `400`, and nothing is written: our hosting's CDN would answer a successful conditional `PATCH` with a `412` of its own after the write ran. To update only the representation you saw, send its tag as `If-Match` to the upsert, `PUT /{collection}/external/{externalId}`.","example":"*"},"required":false,"description":"Only `*`, which always holds here. Any entity tag is `400`, and nothing is written: our hosting's CDN would answer a successful conditional `PATCH` with a `412` of its own after the write ran. To update only the representation you saw, send its tag as `If-Match` to the upsert, `PUT /{collection}/external/{externalId}`.","name":"If-Match","in":"header"},{"schema":{"type":"string","description":"Not taken: any value is `400`, and nothing is written. The record exists by its id, so `*` could never pass. To update only the representation you saw, send its tag as `If-Match` to the upsert, `PUT /{collection}/external/{externalId}`."},"required":false,"description":"Not taken: any value is `400`, and nothing is written. The record exists by its id, so `*` could never pass. To update only the representation you saw, send its tag as `If-Match` to the upsert, `PUT /{collection}/external/{externalId}`.","name":"If-None-Match","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerPatchRequest"}}}},"responses":{"200":{"description":"The customer as it now is, whether or not anything changed.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"ETag":{"description":"Strong validator of the record as `GET /{id}` returns it for this key. Send it as `If-Match` on the next write to make that write conditional.","required":true,"schema":{"type":"string","example":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\""}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Customer"}}}},"400":{"description":"The request was rejected before it ran. `error.code` says why:\n\n- `malformed_json` — the request body is not valid JSON. Check the payload and the `Content-Type` header.\n- `invalid_parameters` — a query, path or body value does not match the schema. Fix the fields `details[]` names.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"invalid_parameters","message":"Invalid request parameters.","details":[{"field":"rateId","code":"invalid_format","message":"`rateId` must be a UUID."}]}},"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"401":{"description":"The key is missing, unknown, revoked or expired, or the organization has switched the Integrations API off, and the response deliberately does not say which. Send a valid key as `Authorization: Bearer sk_tf_int_live_…`. If a key that worked before answers `401`, have an administrator check that it was not revoked or expired and that the API is still switched on.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"WWW-Authenticate":{"description":"Bearer authentication challenge returned on every 401.","required":true,"schema":{"type":"string","example":"Bearer"}}},"content":{"application/json":{"example":{"error":{"code":"unauthorized","message":"Missing or invalid API key. Send it as `Authorization: Bearer sk_tf_int_live_…`."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key is missing the `customers:write` scope this endpoint requires. Scopes are fixed at key creation, so issue a new key carrying the scope named in the message. Also returned as `money_unavailable` when the body sends `rateId` or `rateExternalId` while the key may not see money: without `money:read`, or with the organization's accounting feature switched off. `details[]` says which.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"forbidden_scope","message":"This API key is missing the required scope `customers:write`. Scopes are fixed at key creation — issue a new key to change them."}},"schema":{"anyOf":[{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/MoneyUnavailableError"}]}}}},"404":{"description":"Customer was not found.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"not_found","message":"Customer was not found."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"The write was refused, so nothing was written. `error.code` says why:\n\n- `customer_name_taken` — another customer of the organization already has this name, compared case-insensitively; `details[]` carries its id. Nothing was written. When it is the customer you meant, link it: set your `externalId` on it with `PATCH /customers/{id}`, or send the upsert again with `?adoptExisting=true` (`adoptExisting: true` on a batch). When it is a different customer, choose another name.\n- `external_id_taken` — a `PATCH` sent an `externalId` already linked to another record of the same type. Nothing was written. Clear it on the other record first (`PATCH` with `externalId: null`), or send a different one. `GET /customers?externalId=…`, `GET /projects?externalId=…` or `GET /rates?externalId=…` finds the holder.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"customer_name_taken","message":"Another customer of this organization already has this name (compared case-insensitively); its id is in `details[]`. Link that customer if it is the one you meant, or choose another name.","details":[{"id":"88888888-8888-4888-8888-888888888888","code":"existing_customer"}]}},"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"The request is well-formed but names something this organization cannot use here, so nothing was written. `error.code` says why:\n\n- `rate_not_found` — `rateId` or `rateExternalId` names no hourly rate of this organization. Check the reference against `GET /rates`; when the rate is new, create it first (`PUT /rates/external/{externalId}`).\n- `rate_archived` — `rateId` or `rateExternalId` names an archived rate, which cannot be newly assigned. A record already carrying it may keep it. Choose an active rate (`GET /rates?archived=false`).","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"rate_not_found","message":"`rateId` names no hourly rate of this organization."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"The per-key budget for this window is spent. Failed-authentication throttling carries no per-key `X-RateLimit-*` headers. Wait for `Retry-After`, a delta in seconds, then retry.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"Retry-After":{"description":"Minimum delay before retrying, in seconds (a delta, not Unix epoch seconds).","required":true,"schema":{"type":"integer","minimum":1,"example":30}}},"content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"An unexpected error, with a `requestId` for support. Quote the `requestId` (or `X-Request-Id`) in a support request.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"internal_error","message":"An unexpected error occurred. Quote the request ID when contacting support.","requestId":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/integrations/v1/customers/external/{externalId}":{"get":{"operationId":"getCustomerByExternalId","tags":["Customers"],"summary":"Fetch one customer by externalId","description":"Required scope: `masterdata:read`. The customer linked to your `externalId`, exactly as `GET /customers/{id}` returns it and with the same `ETag`, which is what `If-Match` on `PUT /customers/external/{externalId}` takes. Archived customers are returned too, with `archived: true`. An `externalId` no customer is linked to returns `404`.","x-required-scopes":["masterdata:read"],"x-badges":[{"name":"masterdata:read","position":"before"}],"x-scalar-stability":"stable","parameters":[{"schema":{"type":"string","pattern":"^[\\x21-\\x7e](?:[\\x20-\\x7e]{0,126}[\\x21-\\x7e])?$","example":"K-1001","description":"Your system's identifier for this record: 1–128 printable ASCII characters, no leading or trailing spaces, case-sensitive. Unique per organization and resource; the upsert addresses records by it."},"required":true,"description":"Your system's identifier for this record: 1–128 printable ASCII characters, no leading or trailing spaces, case-sensitive. Unique per organization and resource; the upsert addresses records by it. Percent-encode it in the path.","name":"externalId","in":"path"}],"responses":{"200":{"description":"The customer.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"ETag":{"description":"Strong validator over this exact body. Send it back as `If-None-Match` on the next poll and an unchanged page answers `304` with no body. It covers the whole request — filters, cursor, and whether this key may see money — so tags from two different keys or two different queries never match.","required":true,"schema":{"type":"string","example":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\""}},"Cache-Control":{"description":"`no-cache` means revalidate before reuse, not do not store — it is what makes a conditional request happen. `private` keeps a body scoped to one API key out of shared caches.","required":true,"schema":{"type":"string","example":"private, no-cache"}},"Vary":{"description":"The response depends on the key that asked: two keys for the same organization can see different amounts on the same URL.","required":true,"schema":{"type":"string","example":"Authorization"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Customer"}}}},"304":{"description":"The representation is unchanged since the `ETag` in `If-None-Match`. No body. This still counts against the rate limit — the server ran the query and built the response; only the transfer was skipped.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"ETag":{"description":"Strong validator over this exact body. Send it back as `If-None-Match` on the next poll and an unchanged page answers `304` with no body. It covers the whole request — filters, cursor, and whether this key may see money — so tags from two different keys or two different queries never match.","required":true,"schema":{"type":"string","example":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\""}}}},"400":{"description":"The request was rejected before it ran. `error.code` says why:\n\n- `invalid_parameters` — a query, path or body value does not match the schema. Fix the fields `details[]` names.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"invalid_parameters","message":"Invalid request parameters.","details":[{"field":"limit","code":"too_big","message":"`limit` must be at most 200."}]}},"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"401":{"description":"The key is missing, unknown, revoked or expired, or the organization has switched the Integrations API off, and the response deliberately does not say which. Send a valid key as `Authorization: Bearer sk_tf_int_live_…`. If a key that worked before answers `401`, have an administrator check that it was not revoked or expired and that the API is still switched on.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"WWW-Authenticate":{"description":"Bearer authentication challenge returned on every 401.","required":true,"schema":{"type":"string","example":"Bearer"}}},"content":{"application/json":{"example":{"error":{"code":"unauthorized","message":"Missing or invalid API key. Send it as `Authorization: Bearer sk_tf_int_live_…`."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key is missing the `masterdata:read` scope this endpoint requires. Scopes are fixed at key creation, so issue a new key carrying the scope named in the message.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"forbidden_scope","message":"This API key is missing the required scope `masterdata:read`. Scopes are fixed at key creation — issue a new key to change them."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Customer was not found.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"not_found","message":"Customer was not found."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"The per-key budget for this window is spent. Failed-authentication throttling carries no per-key `X-RateLimit-*` headers. Wait for `Retry-After`, a delta in seconds, then retry.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"Retry-After":{"description":"Minimum delay before retrying, in seconds (a delta, not Unix epoch seconds).","required":true,"schema":{"type":"integer","minimum":1,"example":30}}},"content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"An unexpected error, with a `requestId` for support. Quote the `requestId` (or `X-Request-Id`) in a support request.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"internal_error","message":"An unexpected error occurred. Quote the request ID when contacting support.","requestId":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"schema":{"$ref":"#/components/schemas/Error"}}}}}},"put":{"operationId":"upsertCustomerByExternalId","tags":["Customers"],"summary":"Upsert a customer by externalId","description":"Required scope: `customers:write`. The sync write: updates the customer linked to `externalId`, or creates one linked to it. `201` when it created, `200` otherwise. Safe to send again: an unchanged body writes nothing, and two requests racing to create the same `externalId` create one customer.\n\n**Adopting what was typed in by hand.** When no customer is linked yet and `adoptExisting=true` is sent, the one unlinked customer with the same name (compared case-insensitively) is linked instead, and the body applied to it (`200`). Older customers can share a name in different cases: more than one is `409` `ambiguous_adoption`, listing them in `details[]`. A customer already linked to another `externalId` is never adopted. Without the flag, a name another customer has is `409` `customer_name_taken`, with that customer's id in `details[]`, so you can link it explicitly.\n\n**Preconditions.** `If-None-Match: *` creates only, `If-Match: *` updates only, and `If-Match` with an `ETag` updates only the representation you saw; each answers `412` `precondition_failed` otherwise and writes nothing.","x-required-scopes":["customers:write"],"x-badges":[{"name":"customers:write","position":"before"}],"x-scalar-stability":"stable","parameters":[{"schema":{"type":"string","pattern":"^[\\x21-\\x7e](?:[\\x20-\\x7e]{0,126}[\\x21-\\x7e])?$","example":"K-1001","description":"Your system's identifier for this record: 1–128 printable ASCII characters, no leading or trailing spaces, case-sensitive. Unique per organization and resource; the upsert addresses records by it."},"required":true,"description":"Your system's identifier for this record: 1–128 printable ASCII characters, no leading or trailing spaces, case-sensitive. Unique per organization and resource; the upsert addresses records by it. Percent-encode it in the path.","name":"externalId","in":"path"},{"schema":{"type":"string","enum":["true","false"],"description":"When no record is linked to the `externalId` yet, link an existing unlinked one instead of creating a new one, then apply the body to it. Off by default. See the operation description for how a match is found."},"required":false,"description":"When no record is linked to the `externalId` yet, link an existing unlinked one instead of creating a new one, then apply the body to it. Off by default. See the operation description for how a match is found.","name":"adoptExisting","in":"query"},{"schema":{"type":"string","pattern":"^(?:\\*|(?:W\\/)?\"[\\x21\\x23-\\x7e]*\")$","description":"Optional: the `ETag` of the record as you last read or wrote it. The write runs only if the record still has exactly that representation for this key, and answers `412` `precondition_failed` otherwise, writing nothing. The tag covers the whole body, so renaming what the record names (its rate, its customer, the person or key in `createdBy` or `updatedBy`) also fails it without anyone writing the record: re-read and send again. Without it the last write wins. A weak tag (`W/\"…\"`, as a compressed response carries it) counts as the strong one it was; a list or anything else malformed is `400`. `*` means update only: `412` when no record is linked to the `externalId`. A tag also fails with `412` when none is linked.","example":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\""},"required":false,"description":"Optional: the `ETag` of the record as you last read or wrote it. The write runs only if the record still has exactly that representation for this key, and answers `412` `precondition_failed` otherwise, writing nothing. The tag covers the whole body, so renaming what the record names (its rate, its customer, the person or key in `createdBy` or `updatedBy`) also fails it without anyone writing the record: re-read and send again. Without it the last write wins. A weak tag (`W/\"…\"`, as a compressed response carries it) counts as the strong one it was; a list or anything else malformed is `400`. `*` means update only: `412` when no record is linked to the `externalId`. A tag also fails with `412` when none is linked.","name":"If-Match","in":"header"},{"schema":{"type":"string","pattern":"^\\*$","description":"Optional, and only `*`: create only. `412` `precondition_failed` when a record is already linked to the `externalId`. Adopting an unlinked record with `adoptExisting` still happens, since it was not linked.","example":"*"},"required":false,"description":"Optional, and only `*`: create only. `412` `precondition_failed` when a record is already linked to the `externalId`. Adopting an unlinked record with `adoptExisting` still happens, since it was not linked.","name":"If-None-Match","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerUpsertRequest"}}}},"responses":{"200":{"description":"The customer linked to `externalId` (or adopted for it), as it now is, whether or not anything changed.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"ETag":{"description":"Strong validator of the record as `GET /{id}` returns it for this key. Send it as `If-Match` on the next write to make that write conditional.","required":true,"schema":{"type":"string","example":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\""}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Customer"}}}},"201":{"description":"No customer was linked to `externalId`; this one was created for it.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"ETag":{"description":"Strong validator of the record as `GET /{id}` returns it for this key. Send it as `If-Match` on the next write to make that write conditional.","required":true,"schema":{"type":"string","example":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\""}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Customer"}}}},"400":{"description":"The request was rejected before it ran. `error.code` says why:\n\n- `malformed_json` — the request body is not valid JSON. Check the payload and the `Content-Type` header.\n- `invalid_parameters` — a query, path or body value does not match the schema. Fix the fields `details[]` names.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"invalid_parameters","message":"Invalid request parameters.","details":[{"field":"rateId","code":"invalid_format","message":"`rateId` must be a UUID."}]}},"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"401":{"description":"The key is missing, unknown, revoked or expired, or the organization has switched the Integrations API off, and the response deliberately does not say which. Send a valid key as `Authorization: Bearer sk_tf_int_live_…`. If a key that worked before answers `401`, have an administrator check that it was not revoked or expired and that the API is still switched on.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"WWW-Authenticate":{"description":"Bearer authentication challenge returned on every 401.","required":true,"schema":{"type":"string","example":"Bearer"}}},"content":{"application/json":{"example":{"error":{"code":"unauthorized","message":"Missing or invalid API key. Send it as `Authorization: Bearer sk_tf_int_live_…`."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key is missing the `customers:write` scope this endpoint requires. Scopes are fixed at key creation, so issue a new key carrying the scope named in the message. Also returned as `money_unavailable` when the body sends `rateId` or `rateExternalId` while the key may not see money: without `money:read`, or with the organization's accounting feature switched off. `details[]` says which.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"forbidden_scope","message":"This API key is missing the required scope `customers:write`. Scopes are fixed at key creation — issue a new key to change them."}},"schema":{"anyOf":[{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/MoneyUnavailableError"}]}}}},"409":{"description":"The write was refused, so nothing was written. `error.code` says why:\n\n- `customer_name_taken` — another customer of the organization already has this name, compared case-insensitively; `details[]` carries its id. Nothing was written. When it is the customer you meant, link it: set your `externalId` on it with `PATCH /customers/{id}`, or send the upsert again with `?adoptExisting=true` (`adoptExisting: true` on a batch). When it is a different customer, choose another name.\n- `ambiguous_adoption` — `adoptExisting` found more than one unlinked record to adopt: projects with the body's `projectNumber`, or customers or hourly rates with the body's name, compared case-insensitively. `details[]` lists them. Nothing was written. Link the right record explicitly with `PATCH /projects/{id}`, `PATCH /customers/{id}` or `PATCH /rates/{id}` and its `externalId`.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"customer_name_taken","message":"Another customer of this organization already has this name (compared case-insensitively); its id is in `details[]`. Link that customer if it is the one you meant, or choose another name.","details":[{"id":"88888888-8888-4888-8888-888888888888","code":"existing_customer"}]}},"schema":{"$ref":"#/components/schemas/Error"}}}},"412":{"description":"`If-Match` (a batch item's `ifMatch`) names a representation the record no longer has, or `If-Match: *` / `If-None-Match: *` disagree with whether the `externalId` is linked. Nothing was written. On a stale tag, re-read the record, reconcile the change, and send the write again with the new `ETag`. On `If-None-Match: *` the `externalId` is already linked, and on `If-Match: *` it is not: decide whether to update or create, and resend without the header.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"precondition_failed","message":"The request's `If-Match` or `If-None-Match` precondition does not hold, so nothing was written. Re-read the record and decide again."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"The request is well-formed but names something this organization cannot use here, so nothing was written. `error.code` says why:\n\n- `rate_not_found` — `rateId` or `rateExternalId` names no hourly rate of this organization. Check the reference against `GET /rates`; when the rate is new, create it first (`PUT /rates/external/{externalId}`).\n- `rate_archived` — `rateId` or `rateExternalId` names an archived rate, which cannot be newly assigned. A record already carrying it may keep it. Choose an active rate (`GET /rates?archived=false`).","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"rate_not_found","message":"`rateId` names no hourly rate of this organization."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"The per-key budget for this window is spent. Failed-authentication throttling carries no per-key `X-RateLimit-*` headers. Wait for `Retry-After`, a delta in seconds, then retry.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"Retry-After":{"description":"Minimum delay before retrying, in seconds (a delta, not Unix epoch seconds).","required":true,"schema":{"type":"integer","minimum":1,"example":30}}},"content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"An unexpected error, with a `requestId` for support. Quote the `requestId` (or `X-Request-Id`) in a support request.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"internal_error","message":"An unexpected error occurred. Quote the request ID when contacting support.","requestId":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/integrations/v1/rates":{"get":{"operationId":"listRates","tags":["Hourly rates"],"summary":"List hourly rates","description":"Required scope: `masterdata:read`. Oldest first by creation, in pages walked with `cursor`. `?sort=updatedAt` walks by last modification instead, with `from`/`to` bounding that walk. The organization's stored rates, whatever uses them, including rates assigned only to people. Archived rates are returned and flagged, not hidden. `amount` is null when the key may not see money; every other field stays.","x-required-scopes":["masterdata:read"],"x-badges":[{"name":"masterdata:read","position":"before"}],"x-scalar-stability":"stable","parameters":[{"schema":{"type":"array","items":{"type":"string","format":"uuid"},"minItems":1,"maxItems":100,"description":"Only the rates named here, by TimeFusion's `id`: the list form of `GET /rates/{id}`, for resolving many at once, such as every `rate.id` on a page of bookings. An unknown id contributes no rows rather than an error. Repeat the parameter for several values — `?id=a&id=b` — and a rate matches any of them; at most 100 per request. Repeated values within one parameter are ORed, different parameters are ANDed, and duplicates are ignored. Bracketed keys (`id[]`, `id[0]`) are rejected with `400`.","example":["11111111-1111-4111-8111-111111111111","22222222-2222-4222-8222-222222222222"]},"required":false,"description":"Only the rates named here, by TimeFusion's `id`: the list form of `GET /rates/{id}`, for resolving many at once, such as every `rate.id` on a page of bookings. An unknown id contributes no rows rather than an error. Repeat the parameter for several values — `?id=a&id=b` — and a rate matches any of them; at most 100 per request. Repeated values within one parameter are ORed, different parameters are ANDed, and duplicates are ignored. Bracketed keys (`id[]`, `id[0]`) are rejected with `400`.","name":"id","in":"query","style":"form","explode":true},{"schema":{"type":"array","items":{"type":"string","pattern":"^[\\x21-\\x7e](?:[\\x20-\\x7e]{0,126}[\\x21-\\x7e])?$"},"minItems":1,"maxItems":50,"description":"Only rates linked to this `externalId`, matched exactly (case-sensitive). Repeat the parameter for several values — `?externalId=PL-STD&externalId=PL-SEN` — and a rate matches any of them; at most 50 per request. Repeated values within one parameter are ORed, different parameters are ANDed, and duplicates are ignored. Bracketed keys (`externalId[]`, `externalId[0]`) are rejected with `400`.","example":["PL-STD","PL-SEN"]},"required":false,"description":"Only rates linked to this `externalId`, matched exactly (case-sensitive). Repeat the parameter for several values — `?externalId=PL-STD&externalId=PL-SEN` — and a rate matches any of them; at most 50 per request. Repeated values within one parameter are ORed, different parameters are ANDed, and duplicates are ignored. Bracketed keys (`externalId[]`, `externalId[0]`) are rejected with `400`.","name":"externalId","in":"query","style":"form","explode":true},{"schema":{"type":"string","enum":["true","false"],"description":"Filter by the archived flag. Omit to receive both."},"required":false,"description":"Filter by the archived flag. Omit to receive both.","name":"archived","in":"query"},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}([T ]\\d{2}:\\d{2}(:\\d{2}(\\.\\d{1,3})?)?(Z|[+-]\\d{2}:\\d{2})?)?$","description":"Inclusive start of the walk. **Only valid with `sort=updatedAt`**, where it selects rows last written at or after this instant; rejected as `400 invalid_parameters` with the default `sort=createdAt`. A date without a time means 00:00:00.000 UTC on that day. Timestamps without an offset are interpreted as UTC; explicit offsets are respected.","example":"2026-08-01"},"required":false,"description":"Inclusive start of the walk. **Only valid with `sort=updatedAt`**, where it selects rows last written at or after this instant; rejected as `400 invalid_parameters` with the default `sort=createdAt`. A date without a time means 00:00:00.000 UTC on that day. Timestamps without an offset are interpreted as UTC; explicit offsets are respected.","name":"from","in":"query"},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}([T ]\\d{2}:\\d{2}(:\\d{2}(\\.\\d{1,3})?)?(Z|[+-]\\d{2}:\\d{2})?)?$","description":"Inclusive end of the walk. **Only valid with `sort=updatedAt`**, where it selects rows last written at or before this instant; rejected as `400 invalid_parameters` with the default `sort=createdAt`. A date without a time means 23:59:59.999 UTC on that day. Timestamps without an offset are interpreted as UTC; explicit offsets are respected.","example":"2026-08-31"},"required":false,"description":"Inclusive end of the walk. **Only valid with `sort=updatedAt`**, where it selects rows last written at or before this instant; rejected as `400 invalid_parameters` with the default `sort=createdAt`. A date without a time means 23:59:59.999 UTC on that day. Timestamps without an offset are interpreted as UTC; explicit offsets are respected.","name":"to","in":"query"},{"schema":{"type":"string","enum":["createdAt","updatedAt"],"default":"createdAt","description":"Which order to walk, and which timestamp `from`/`to` bound. `createdAt` (default) walks oldest-created first. `updatedAt` walks by last modification, oldest first — the ordering a mirror polls to find which rows changed. `from` and `to` are only accepted with `updatedAt`. Cursors are not interchangeable between the two.","example":"createdAt"},"required":false,"description":"Which order to walk, and which timestamp `from`/`to` bound. `createdAt` (default) walks oldest-created first. `updatedAt` walks by last modification, oldest first — the ordering a mirror polls to find which rows changed. `from` and `to` are only accepted with `updatedAt`. Cursors are not interchangeable between the two.","name":"sort","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":200,"default":50,"description":"Page size, 1–200. Values above 200 are rejected, not clamped.","example":50},"required":false,"description":"Page size, 1–200. Values above 200 are rejected, not clamped.","name":"limit","in":"query"},{"schema":{"type":"string","description":"Opaque cursor from a previous page's `pagination.nextCursor` **on this endpoint**. Cursors are not interchangeable between endpoints; one issued elsewhere is rejected as `invalid_cursor`. Omit for the first page."},"required":false,"description":"Opaque cursor from a previous page's `pagination.nextCursor` **on this endpoint**. Cursors are not interchangeable between endpoints; one issued elsewhere is rejected as `invalid_cursor`. Omit for the first page.","name":"cursor","in":"query"}],"responses":{"200":{"description":"One page of hourly rates.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"ETag":{"description":"Strong validator over this exact body. Send it back as `If-None-Match` on the next poll and an unchanged page answers `304` with no body. It covers the whole request — filters, cursor, and whether this key may see money — so tags from two different keys or two different queries never match.","required":true,"schema":{"type":"string","example":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\""}},"Cache-Control":{"description":"`no-cache` means revalidate before reuse, not do not store — it is what makes a conditional request happen. `private` keeps a body scoped to one API key out of shared caches.","required":true,"schema":{"type":"string","example":"private, no-cache"}},"Vary":{"description":"The response depends on the key that asked: two keys for the same organization can see different amounts on the same URL.","required":true,"schema":{"type":"string","example":"Authorization"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateCatalogEntryPage"}}}},"304":{"description":"The representation is unchanged since the `ETag` in `If-None-Match`. No body. This still counts against the rate limit — the server ran the query and built the response; only the transfer was skipped.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"ETag":{"description":"Strong validator over this exact body. Send it back as `If-None-Match` on the next poll and an unchanged page answers `304` with no body. It covers the whole request — filters, cursor, and whether this key may see money — so tags from two different keys or two different queries never match.","required":true,"schema":{"type":"string","example":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\""}}}},"400":{"description":"The request was rejected before it ran. `error.code` says why:\n\n- `invalid_parameters` — a query, path or body value does not match the schema. Fix the fields `details[]` names.\n- `invalid_period` — `from` or `to` is date-shaped but not a real date (`2026-13-01`, `2026-02-31`), or `to` is earlier than `from`. A boundary that is not date-shaped at all fails the schema as `invalid_parameters` instead. Send a real calendar date, with `to` no earlier than `from`.\n- `invalid_cursor` — the `cursor` was not issued by this API. Discard the cursor and restart the walk from the first page.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"invalid_parameters","message":"Invalid request parameters.","details":[{"field":"limit","code":"too_big","message":"`limit` must be at most 200."}]}},"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"401":{"description":"The key is missing, unknown, revoked or expired, or the organization has switched the Integrations API off, and the response deliberately does not say which. Send a valid key as `Authorization: Bearer sk_tf_int_live_…`. If a key that worked before answers `401`, have an administrator check that it was not revoked or expired and that the API is still switched on.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"WWW-Authenticate":{"description":"Bearer authentication challenge returned on every 401.","required":true,"schema":{"type":"string","example":"Bearer"}}},"content":{"application/json":{"example":{"error":{"code":"unauthorized","message":"Missing or invalid API key. Send it as `Authorization: Bearer sk_tf_int_live_…`."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key is missing the `masterdata:read` scope this endpoint requires. Scopes are fixed at key creation, so issue a new key carrying the scope named in the message.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"forbidden_scope","message":"This API key is missing the required scope `masterdata:read`. Scopes are fixed at key creation — issue a new key to change them."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"The per-key budget for this window is spent. Failed-authentication throttling carries no per-key `X-RateLimit-*` headers. Wait for `Retry-After`, a delta in seconds, then retry.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"Retry-After":{"description":"Minimum delay before retrying, in seconds (a delta, not Unix epoch seconds).","required":true,"schema":{"type":"integer","minimum":1,"example":30}}},"content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"An unexpected error, with a `requestId` for support. Quote the `requestId` (or `X-Request-Id`) in a support request.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"internal_error","message":"An unexpected error occurred. Quote the request ID when contacting support.","requestId":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"createRate","tags":["Hourly rates"],"summary":"Create an hourly rate","description":"Required scope: `rates:write`. Creates an hourly rate, attributed to this key, and answers with it as `GET /rates/{id}` returns it, plus its `ETag`. A new rate is active unless `archived` is sent. Names are not unique, so without an `Idempotency-Key` a second identical request is a second rate. To link the rate to an id of your own system, use `PUT /rates/external/{externalId}` instead, which is safe to send twice without a key.","x-required-scopes":["rates:write"],"x-badges":[{"name":"rates:write","position":"before"}],"x-scalar-stability":"stable","parameters":[{"schema":{"type":"string","pattern":"^[\\x20-\\x7e]{1,128}$","description":"Optional: 1–128 printable ASCII characters, case-sensitive. Send a unique key — a UUID is ideal — to make this write safely retryable: if you never see the response, send the **identical** request with the **same** key and the original result is returned instead of executing again. Keys are scoped to the organization and operation; any currently authorized API key in that organization may replay the result. Receipts are retained for 7 days from execution and describe that original execution, not the records' current state; a key without `money:read` is shown it without rates. A replay carries `Idempotency-Replayed: true`. The same key with a different body is `409 idempotency_key_reused`. Without this header a retry creates a second record.","example":"3f8c1b9e-2d47-4a1e-9a1b-5f6f0f7c2a10"},"required":false,"description":"Optional: 1–128 printable ASCII characters, case-sensitive. Send a unique key — a UUID is ideal — to make this write safely retryable: if you never see the response, send the **identical** request with the **same** key and the original result is returned instead of executing again. Keys are scoped to the organization and operation; any currently authorized API key in that organization may replay the result. Receipts are retained for 7 days from execution and describe that original execution, not the records' current state; a key without `money:read` is shown it without rates. A replay carries `Idempotency-Replayed: true`. The same key with a different body is `409 idempotency_key_reused`. Without this header a retry creates a second record.","name":"Idempotency-Key","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateCreateRequest"}}}},"responses":{"201":{"description":"The rate was created. With `Idempotency-Replayed: true`, this is the retained answer of an earlier execution: the rate as it was then.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"ETag":{"description":"Strong validator of the record as `GET /{id}` returns it for this key. Send it as `If-Match` on the next write to make that write conditional.","required":true,"schema":{"type":"string","example":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\""}},"Idempotency-Replayed":{"description":"Sent as `true` when this response was replayed from a retained receipt rather than executed. Absent on a first execution. The body is the original one, so it describes that execution and not the records' current state.","schema":{"type":"string","example":"true"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateCatalogEntry"}}}},"400":{"description":"The request was rejected before it ran. `error.code` says why:\n\n- `malformed_json` — the request body is not valid JSON. Check the payload and the `Content-Type` header.\n- `invalid_parameters` — a query, path or body value does not match the schema. Fix the fields `details[]` names.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"invalid_parameters","message":"Invalid request parameters.","details":[{"field":"amount","code":"not_multiple_of","message":"`amount` must be a multiple of 0.01."}]}},"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"401":{"description":"The key is missing, unknown, revoked or expired, or the organization has switched the Integrations API off, and the response deliberately does not say which. Send a valid key as `Authorization: Bearer sk_tf_int_live_…`. If a key that worked before answers `401`, have an administrator check that it was not revoked or expired and that the API is still switched on.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"WWW-Authenticate":{"description":"Bearer authentication challenge returned on every 401.","required":true,"schema":{"type":"string","example":"Bearer"}}},"content":{"application/json":{"example":{"error":{"code":"unauthorized","message":"Missing or invalid API key. Send it as `Authorization: Bearer sk_tf_int_live_…`."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key is missing the `rates:write` scope this endpoint requires. Scopes are fixed at key creation, so issue a new key carrying the scope named in the message. Also returned as `money_unavailable` on every rate write while the key may not see money: without `money:read`, or with the organization's accounting feature switched off. `details[]` says which.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"forbidden_scope","message":"This API key is missing the required scope `rates:write`. Scopes are fixed at key creation — issue a new key to change them."}},"schema":{"anyOf":[{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/MoneyUnavailableError"}]}}}},"409":{"description":"The write was refused, so nothing was written. `error.code` says why:\n\n- `idempotency_key_reused` — this `Idempotency-Key` is retained against a different request body. Receipts are kept for 7 days. Send the original request unchanged, or a new key for a new intent.\n- `idempotency_request_in_progress` — another request holding this key is still running and did not finish within the bounded wait. **Retry** the identical request after a few seconds; it replays that request's result rather than executing twice.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"idempotency_key_reused","message":"This `Idempotency-Key` was already used for a different request body. Retry the original request unchanged, or generate a new key for the new request."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"The per-key budget for this window is spent. Failed-authentication throttling carries no per-key `X-RateLimit-*` headers. Wait for `Retry-After`, a delta in seconds, then retry.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"Retry-After":{"description":"Minimum delay before retrying, in seconds (a delta, not Unix epoch seconds).","required":true,"schema":{"type":"integer","minimum":1,"example":30}}},"content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"An unexpected error, with a `requestId` for support. Quote the `requestId` (or `X-Request-Id`) in a support request.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"internal_error","message":"An unexpected error occurred. Quote the request ID when contacting support.","requestId":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/integrations/v1/rates/{id}":{"get":{"operationId":"getRate","tags":["Hourly rates"],"summary":"Fetch one hourly rate by id","description":"Required scope: `masterdata:read`. A single stored rate by id, the target of every `rate.id` this API publishes. Archived rates are returned too, with `archived: true`; an unknown id returns `404`.","x-required-scopes":["masterdata:read"],"x-badges":[{"name":"masterdata:read","position":"before"}],"x-scalar-stability":"stable","parameters":[{"schema":{"type":"string","format":"uuid","description":"Stable identifier of the hourly rate to retrieve."},"required":true,"description":"Stable identifier of the hourly rate to retrieve.","name":"id","in":"path"}],"responses":{"200":{"description":"The hourly rate.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"ETag":{"description":"Strong validator over this exact body. Send it back as `If-None-Match` on the next poll and an unchanged page answers `304` with no body. It covers the whole request — filters, cursor, and whether this key may see money — so tags from two different keys or two different queries never match.","required":true,"schema":{"type":"string","example":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\""}},"Cache-Control":{"description":"`no-cache` means revalidate before reuse, not do not store — it is what makes a conditional request happen. `private` keeps a body scoped to one API key out of shared caches.","required":true,"schema":{"type":"string","example":"private, no-cache"}},"Vary":{"description":"The response depends on the key that asked: two keys for the same organization can see different amounts on the same URL.","required":true,"schema":{"type":"string","example":"Authorization"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateCatalogEntry"}}}},"304":{"description":"The representation is unchanged since the `ETag` in `If-None-Match`. No body. This still counts against the rate limit — the server ran the query and built the response; only the transfer was skipped.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"ETag":{"description":"Strong validator over this exact body. Send it back as `If-None-Match` on the next poll and an unchanged page answers `304` with no body. It covers the whole request — filters, cursor, and whether this key may see money — so tags from two different keys or two different queries never match.","required":true,"schema":{"type":"string","example":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\""}}}},"400":{"description":"The request was rejected before it ran. `error.code` says why:\n\n- `invalid_parameters` — a query, path or body value does not match the schema. Fix the fields `details[]` names.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"invalid_parameters","message":"Invalid request parameters.","details":[{"field":"limit","code":"too_big","message":"`limit` must be at most 200."}]}},"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"401":{"description":"The key is missing, unknown, revoked or expired, or the organization has switched the Integrations API off, and the response deliberately does not say which. Send a valid key as `Authorization: Bearer sk_tf_int_live_…`. If a key that worked before answers `401`, have an administrator check that it was not revoked or expired and that the API is still switched on.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"WWW-Authenticate":{"description":"Bearer authentication challenge returned on every 401.","required":true,"schema":{"type":"string","example":"Bearer"}}},"content":{"application/json":{"example":{"error":{"code":"unauthorized","message":"Missing or invalid API key. Send it as `Authorization: Bearer sk_tf_int_live_…`."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key is missing the `masterdata:read` scope this endpoint requires. Scopes are fixed at key creation, so issue a new key carrying the scope named in the message.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"forbidden_scope","message":"This API key is missing the required scope `masterdata:read`. Scopes are fixed at key creation — issue a new key to change them."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Rate was not found.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"not_found","message":"Rate was not found."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"The per-key budget for this window is spent. Failed-authentication throttling carries no per-key `X-RateLimit-*` headers. Wait for `Retry-After`, a delta in seconds, then retry.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"Retry-After":{"description":"Minimum delay before retrying, in seconds (a delta, not Unix epoch seconds).","required":true,"schema":{"type":"integer","minimum":1,"example":30}}},"content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"An unexpected error, with a `requestId` for support. Quote the `requestId` (or `X-Request-Id`) in a support request.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"internal_error","message":"An unexpected error occurred. Quote the request ID when contacting support.","requestId":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"operationId":"updateRate","tags":["Hourly rates"],"summary":"Update an hourly rate by id","description":"Required scope: `rates:write`. Changes the fields the body sends and leaves the others as they are; `null` clears a nullable field. A body that changes nothing writes nothing: `updatedAt` and the `ETag` stay put, which is what keeps two systems syncing in both directions from echoing one change back and forth. Otherwise the change is attributed to this key. A changed `amount` re-prices every booking not yet billed that resolves to this rate. Archiving and reactivating are the `archived` field. `externalId` links, re-links or (with `null`) unlinks the rate. A `PATCH` takes no entity tag: to refuse the write when the rate changed since you read it, send its `ETag` as `If-Match` to the upsert by `externalId` instead.","x-required-scopes":["rates:write"],"x-badges":[{"name":"rates:write","position":"before"}],"x-scalar-stability":"stable","parameters":[{"schema":{"type":"string","format":"uuid","description":"Stable identifier of the hourly rate to retrieve."},"required":true,"description":"Stable identifier of the hourly rate to retrieve.","name":"id","in":"path"},{"schema":{"type":"string","pattern":"^\\*$","description":"Only `*`, which always holds here. Any entity tag is `400`, and nothing is written: our hosting's CDN would answer a successful conditional `PATCH` with a `412` of its own after the write ran. To update only the representation you saw, send its tag as `If-Match` to the upsert, `PUT /{collection}/external/{externalId}`.","example":"*"},"required":false,"description":"Only `*`, which always holds here. Any entity tag is `400`, and nothing is written: our hosting's CDN would answer a successful conditional `PATCH` with a `412` of its own after the write ran. To update only the representation you saw, send its tag as `If-Match` to the upsert, `PUT /{collection}/external/{externalId}`.","name":"If-Match","in":"header"},{"schema":{"type":"string","description":"Not taken: any value is `400`, and nothing is written. The record exists by its id, so `*` could never pass. To update only the representation you saw, send its tag as `If-Match` to the upsert, `PUT /{collection}/external/{externalId}`."},"required":false,"description":"Not taken: any value is `400`, and nothing is written. The record exists by its id, so `*` could never pass. To update only the representation you saw, send its tag as `If-Match` to the upsert, `PUT /{collection}/external/{externalId}`.","name":"If-None-Match","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RatePatchRequest"}}}},"responses":{"200":{"description":"The rate as it now is, whether or not anything changed.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"ETag":{"description":"Strong validator of the record as `GET /{id}` returns it for this key. Send it as `If-Match` on the next write to make that write conditional.","required":true,"schema":{"type":"string","example":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\""}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateCatalogEntry"}}}},"400":{"description":"The request was rejected before it ran. `error.code` says why:\n\n- `malformed_json` — the request body is not valid JSON. Check the payload and the `Content-Type` header.\n- `invalid_parameters` — a query, path or body value does not match the schema. Fix the fields `details[]` names.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"invalid_parameters","message":"Invalid request parameters.","details":[{"field":"amount","code":"not_multiple_of","message":"`amount` must be a multiple of 0.01."}]}},"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"401":{"description":"The key is missing, unknown, revoked or expired, or the organization has switched the Integrations API off, and the response deliberately does not say which. Send a valid key as `Authorization: Bearer sk_tf_int_live_…`. If a key that worked before answers `401`, have an administrator check that it was not revoked or expired and that the API is still switched on.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"WWW-Authenticate":{"description":"Bearer authentication challenge returned on every 401.","required":true,"schema":{"type":"string","example":"Bearer"}}},"content":{"application/json":{"example":{"error":{"code":"unauthorized","message":"Missing or invalid API key. Send it as `Authorization: Bearer sk_tf_int_live_…`."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key is missing the `rates:write` scope this endpoint requires. Scopes are fixed at key creation, so issue a new key carrying the scope named in the message. Also returned as `money_unavailable` on every rate write while the key may not see money: without `money:read`, or with the organization's accounting feature switched off. `details[]` says which.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"forbidden_scope","message":"This API key is missing the required scope `rates:write`. Scopes are fixed at key creation — issue a new key to change them."}},"schema":{"anyOf":[{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/MoneyUnavailableError"}]}}}},"404":{"description":"Rate was not found.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"not_found","message":"Rate was not found."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"The write was refused, so nothing was written. `error.code` says why:\n\n- `external_id_taken` — a `PATCH` sent an `externalId` already linked to another record of the same type. Nothing was written. Clear it on the other record first (`PATCH` with `externalId: null`), or send a different one. `GET /customers?externalId=…`, `GET /projects?externalId=…` or `GET /rates?externalId=…` finds the holder.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"external_id_taken","message":"This `externalId` is already linked to another record of this type. Unlink it there first, or send a different one."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"The per-key budget for this window is spent. Failed-authentication throttling carries no per-key `X-RateLimit-*` headers. Wait for `Retry-After`, a delta in seconds, then retry.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"Retry-After":{"description":"Minimum delay before retrying, in seconds (a delta, not Unix epoch seconds).","required":true,"schema":{"type":"integer","minimum":1,"example":30}}},"content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"An unexpected error, with a `requestId` for support. Quote the `requestId` (or `X-Request-Id`) in a support request.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"internal_error","message":"An unexpected error occurred. Quote the request ID when contacting support.","requestId":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/integrations/v1/rates/external/{externalId}":{"get":{"operationId":"getRateByExternalId","tags":["Hourly rates"],"summary":"Fetch one hourly rate by externalId","description":"Required scope: `masterdata:read`. The hourly rate linked to your `externalId`, exactly as `GET /rates/{id}` returns it and with the same `ETag`, which is what `If-Match` on `PUT /rates/external/{externalId}` takes. Archived rates are returned too, with `archived: true`. An `externalId` no hourly rate is linked to returns `404`.","x-required-scopes":["masterdata:read"],"x-badges":[{"name":"masterdata:read","position":"before"}],"x-scalar-stability":"stable","parameters":[{"schema":{"type":"string","pattern":"^[\\x21-\\x7e](?:[\\x20-\\x7e]{0,126}[\\x21-\\x7e])?$","example":"K-1001","description":"Your system's identifier for this record: 1–128 printable ASCII characters, no leading or trailing spaces, case-sensitive. Unique per organization and resource; the upsert addresses records by it."},"required":true,"description":"Your system's identifier for this record: 1–128 printable ASCII characters, no leading or trailing spaces, case-sensitive. Unique per organization and resource; the upsert addresses records by it. Percent-encode it in the path.","name":"externalId","in":"path"}],"responses":{"200":{"description":"The hourly rate.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"ETag":{"description":"Strong validator over this exact body. Send it back as `If-None-Match` on the next poll and an unchanged page answers `304` with no body. It covers the whole request — filters, cursor, and whether this key may see money — so tags from two different keys or two different queries never match.","required":true,"schema":{"type":"string","example":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\""}},"Cache-Control":{"description":"`no-cache` means revalidate before reuse, not do not store — it is what makes a conditional request happen. `private` keeps a body scoped to one API key out of shared caches.","required":true,"schema":{"type":"string","example":"private, no-cache"}},"Vary":{"description":"The response depends on the key that asked: two keys for the same organization can see different amounts on the same URL.","required":true,"schema":{"type":"string","example":"Authorization"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateCatalogEntry"}}}},"304":{"description":"The representation is unchanged since the `ETag` in `If-None-Match`. No body. This still counts against the rate limit — the server ran the query and built the response; only the transfer was skipped.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"ETag":{"description":"Strong validator over this exact body. Send it back as `If-None-Match` on the next poll and an unchanged page answers `304` with no body. It covers the whole request — filters, cursor, and whether this key may see money — so tags from two different keys or two different queries never match.","required":true,"schema":{"type":"string","example":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\""}}}},"400":{"description":"The request was rejected before it ran. `error.code` says why:\n\n- `invalid_parameters` — a query, path or body value does not match the schema. Fix the fields `details[]` names.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"invalid_parameters","message":"Invalid request parameters.","details":[{"field":"limit","code":"too_big","message":"`limit` must be at most 200."}]}},"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"401":{"description":"The key is missing, unknown, revoked or expired, or the organization has switched the Integrations API off, and the response deliberately does not say which. Send a valid key as `Authorization: Bearer sk_tf_int_live_…`. If a key that worked before answers `401`, have an administrator check that it was not revoked or expired and that the API is still switched on.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"WWW-Authenticate":{"description":"Bearer authentication challenge returned on every 401.","required":true,"schema":{"type":"string","example":"Bearer"}}},"content":{"application/json":{"example":{"error":{"code":"unauthorized","message":"Missing or invalid API key. Send it as `Authorization: Bearer sk_tf_int_live_…`."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key is missing the `masterdata:read` scope this endpoint requires. Scopes are fixed at key creation, so issue a new key carrying the scope named in the message.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"forbidden_scope","message":"This API key is missing the required scope `masterdata:read`. Scopes are fixed at key creation — issue a new key to change them."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Rate was not found.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"not_found","message":"Rate was not found."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"The per-key budget for this window is spent. Failed-authentication throttling carries no per-key `X-RateLimit-*` headers. Wait for `Retry-After`, a delta in seconds, then retry.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"Retry-After":{"description":"Minimum delay before retrying, in seconds (a delta, not Unix epoch seconds).","required":true,"schema":{"type":"integer","minimum":1,"example":30}}},"content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"An unexpected error, with a `requestId` for support. Quote the `requestId` (or `X-Request-Id`) in a support request.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"internal_error","message":"An unexpected error occurred. Quote the request ID when contacting support.","requestId":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"schema":{"$ref":"#/components/schemas/Error"}}}}}},"put":{"operationId":"upsertRateByExternalId","tags":["Hourly rates"],"summary":"Upsert an hourly rate by externalId","description":"Required scope: `rates:write`. The sync write: updates the rate linked to `externalId`, or creates one linked to it. `201` when it created, `200` otherwise. Safe to send again: an unchanged body writes nothing, and two requests racing to create the same `externalId` create one rate. Once linked, customers and projects name it as `rateExternalId`, so your system never has to store TimeFusion's rate ids.\n\n**Adopting what was typed in by hand.** When no rate is linked yet and `adoptExisting=true` is sent, the one unlinked rate with the same name (compared case-insensitively) is linked instead, and the body applied to it (`200`). Rate names are not unique: more than one is `409` `ambiguous_adoption`, listing them in `details[]`; none creates. A rate already linked to another `externalId` is never adopted.\n\n**Preconditions.** `If-None-Match: *` creates only, `If-Match: *` updates only, and `If-Match` with an `ETag` updates only the representation you saw; each answers `412` `precondition_failed` otherwise and writes nothing.","x-required-scopes":["rates:write"],"x-badges":[{"name":"rates:write","position":"before"}],"x-scalar-stability":"stable","parameters":[{"schema":{"type":"string","pattern":"^[\\x21-\\x7e](?:[\\x20-\\x7e]{0,126}[\\x21-\\x7e])?$","example":"K-1001","description":"Your system's identifier for this record: 1–128 printable ASCII characters, no leading or trailing spaces, case-sensitive. Unique per organization and resource; the upsert addresses records by it."},"required":true,"description":"Your system's identifier for this record: 1–128 printable ASCII characters, no leading or trailing spaces, case-sensitive. Unique per organization and resource; the upsert addresses records by it. Percent-encode it in the path.","name":"externalId","in":"path"},{"schema":{"type":"string","enum":["true","false"],"description":"When no record is linked to the `externalId` yet, link an existing unlinked one instead of creating a new one, then apply the body to it. Off by default. See the operation description for how a match is found."},"required":false,"description":"When no record is linked to the `externalId` yet, link an existing unlinked one instead of creating a new one, then apply the body to it. Off by default. See the operation description for how a match is found.","name":"adoptExisting","in":"query"},{"schema":{"type":"string","pattern":"^(?:\\*|(?:W\\/)?\"[\\x21\\x23-\\x7e]*\")$","description":"Optional: the `ETag` of the record as you last read or wrote it. The write runs only if the record still has exactly that representation for this key, and answers `412` `precondition_failed` otherwise, writing nothing. The tag covers the whole body, so renaming what the record names (its rate, its customer, the person or key in `createdBy` or `updatedBy`) also fails it without anyone writing the record: re-read and send again. Without it the last write wins. A weak tag (`W/\"…\"`, as a compressed response carries it) counts as the strong one it was; a list or anything else malformed is `400`. `*` means update only: `412` when no record is linked to the `externalId`. A tag also fails with `412` when none is linked.","example":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\""},"required":false,"description":"Optional: the `ETag` of the record as you last read or wrote it. The write runs only if the record still has exactly that representation for this key, and answers `412` `precondition_failed` otherwise, writing nothing. The tag covers the whole body, so renaming what the record names (its rate, its customer, the person or key in `createdBy` or `updatedBy`) also fails it without anyone writing the record: re-read and send again. Without it the last write wins. A weak tag (`W/\"…\"`, as a compressed response carries it) counts as the strong one it was; a list or anything else malformed is `400`. `*` means update only: `412` when no record is linked to the `externalId`. A tag also fails with `412` when none is linked.","name":"If-Match","in":"header"},{"schema":{"type":"string","pattern":"^\\*$","description":"Optional, and only `*`: create only. `412` `precondition_failed` when a record is already linked to the `externalId`. Adopting an unlinked record with `adoptExisting` still happens, since it was not linked.","example":"*"},"required":false,"description":"Optional, and only `*`: create only. `412` `precondition_failed` when a record is already linked to the `externalId`. Adopting an unlinked record with `adoptExisting` still happens, since it was not linked.","name":"If-None-Match","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateUpsertRequest"}}}},"responses":{"200":{"description":"The rate linked to `externalId` (or adopted for it), as it now is, whether or not anything changed.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"ETag":{"description":"Strong validator of the record as `GET /{id}` returns it for this key. Send it as `If-Match` on the next write to make that write conditional.","required":true,"schema":{"type":"string","example":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\""}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateCatalogEntry"}}}},"201":{"description":"No rate was linked to `externalId`; this one was created for it.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"ETag":{"description":"Strong validator of the record as `GET /{id}` returns it for this key. Send it as `If-Match` on the next write to make that write conditional.","required":true,"schema":{"type":"string","example":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\""}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateCatalogEntry"}}}},"400":{"description":"The request was rejected before it ran. `error.code` says why:\n\n- `malformed_json` — the request body is not valid JSON. Check the payload and the `Content-Type` header.\n- `invalid_parameters` — a query, path or body value does not match the schema. Fix the fields `details[]` names.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"invalid_parameters","message":"Invalid request parameters.","details":[{"field":"amount","code":"not_multiple_of","message":"`amount` must be a multiple of 0.01."}]}},"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"401":{"description":"The key is missing, unknown, revoked or expired, or the organization has switched the Integrations API off, and the response deliberately does not say which. Send a valid key as `Authorization: Bearer sk_tf_int_live_…`. If a key that worked before answers `401`, have an administrator check that it was not revoked or expired and that the API is still switched on.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"WWW-Authenticate":{"description":"Bearer authentication challenge returned on every 401.","required":true,"schema":{"type":"string","example":"Bearer"}}},"content":{"application/json":{"example":{"error":{"code":"unauthorized","message":"Missing or invalid API key. Send it as `Authorization: Bearer sk_tf_int_live_…`."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key is missing the `rates:write` scope this endpoint requires. Scopes are fixed at key creation, so issue a new key carrying the scope named in the message. Also returned as `money_unavailable` on every rate write while the key may not see money: without `money:read`, or with the organization's accounting feature switched off. `details[]` says which.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"forbidden_scope","message":"This API key is missing the required scope `rates:write`. Scopes are fixed at key creation — issue a new key to change them."}},"schema":{"anyOf":[{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/MoneyUnavailableError"}]}}}},"409":{"description":"The write was refused, so nothing was written. `error.code` says why:\n\n- `ambiguous_adoption` — `adoptExisting` found more than one unlinked record to adopt: projects with the body's `projectNumber`, or customers or hourly rates with the body's name, compared case-insensitively. `details[]` lists them. Nothing was written. Link the right record explicitly with `PATCH /projects/{id}`, `PATCH /customers/{id}` or `PATCH /rates/{id}` and its `externalId`.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"ambiguous_adoption","message":"More than one unlinked hourly rate has this name (compared case-insensitively), so none was adopted. Link the right one with `PATCH /rates/{id}`: the candidates are in `details[]`.","details":[{"id":"aa000001-0000-4000-8000-000000000001","code":"adoption_candidate"},{"id":"aa000001-0000-4000-8000-000000000009","code":"adoption_candidate"}]}},"schema":{"$ref":"#/components/schemas/Error"}}}},"412":{"description":"`If-Match` (a batch item's `ifMatch`) names a representation the record no longer has, or `If-Match: *` / `If-None-Match: *` disagree with whether the `externalId` is linked. Nothing was written. On a stale tag, re-read the record, reconcile the change, and send the write again with the new `ETag`. On `If-None-Match: *` the `externalId` is already linked, and on `If-Match: *` it is not: decide whether to update or create, and resend without the header.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"precondition_failed","message":"The request's `If-Match` or `If-None-Match` precondition does not hold, so nothing was written. Re-read the record and decide again."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"The per-key budget for this window is spent. Failed-authentication throttling carries no per-key `X-RateLimit-*` headers. Wait for `Retry-After`, a delta in seconds, then retry.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"Retry-After":{"description":"Minimum delay before retrying, in seconds (a delta, not Unix epoch seconds).","required":true,"schema":{"type":"integer","minimum":1,"example":30}}},"content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"An unexpected error, with a `requestId` for support. Quote the `requestId` (or `X-Request-Id`) in a support request.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"internal_error","message":"An unexpected error occurred. Quote the request ID when contacting support.","requestId":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/integrations/v1/users":{"get":{"operationId":"listUsers","tags":["People"],"summary":"List people","description":"Required scope: `users:read`. The organization's people, for mapping `TimeBooking.user.id` to your own employee records ahead of the first sync. Oldest first by creation, in pages walked with `cursor`. Deactivated people are returned and flagged, not hidden.","x-required-scopes":["users:read"],"x-badges":[{"name":"users:read","position":"before"}],"x-scalar-stability":"stable","parameters":[{"schema":{"type":"string","enum":["true","false"],"description":"Filter by the deactivated flag. Omit to receive both. `false` includes people whose flag was never set, matching the `deactivated` a person publishes."},"required":false,"description":"Filter by the deactivated flag. Omit to receive both. `false` includes people whose flag was never set, matching the `deactivated` a person publishes.","name":"deactivated","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":200,"default":50,"description":"Page size, 1–200. Values above 200 are rejected, not clamped.","example":50},"required":false,"description":"Page size, 1–200. Values above 200 are rejected, not clamped.","name":"limit","in":"query"},{"schema":{"type":"string","description":"Opaque cursor from a previous page's `pagination.nextCursor` **on this endpoint**. Cursors are not interchangeable between endpoints; one issued elsewhere is rejected as `invalid_cursor`. Omit for the first page."},"required":false,"description":"Opaque cursor from a previous page's `pagination.nextCursor` **on this endpoint**. Cursors are not interchangeable between endpoints; one issued elsewhere is rejected as `invalid_cursor`. Omit for the first page.","name":"cursor","in":"query"}],"responses":{"200":{"description":"One page of people.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"ETag":{"description":"Strong validator over this exact body. Send it back as `If-None-Match` on the next poll and an unchanged page answers `304` with no body. It covers the whole request — filters, cursor, and whether this key may see money — so tags from two different keys or two different queries never match.","required":true,"schema":{"type":"string","example":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\""}},"Cache-Control":{"description":"`no-cache` means revalidate before reuse, not do not store — it is what makes a conditional request happen. `private` keeps a body scoped to one API key out of shared caches.","required":true,"schema":{"type":"string","example":"private, no-cache"}},"Vary":{"description":"The response depends on the key that asked: two keys for the same organization can see different amounts on the same URL.","required":true,"schema":{"type":"string","example":"Authorization"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserPage"}}}},"304":{"description":"The representation is unchanged since the `ETag` in `If-None-Match`. No body. This still counts against the rate limit — the server ran the query and built the response; only the transfer was skipped.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"ETag":{"description":"Strong validator over this exact body. Send it back as `If-None-Match` on the next poll and an unchanged page answers `304` with no body. It covers the whole request — filters, cursor, and whether this key may see money — so tags from two different keys or two different queries never match.","required":true,"schema":{"type":"string","example":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\""}}}},"400":{"description":"The request was rejected before it ran. `error.code` says why:\n\n- `invalid_parameters` — a query, path or body value does not match the schema. Fix the fields `details[]` names.\n- `invalid_cursor` — the `cursor` was not issued by this API. Discard the cursor and restart the walk from the first page.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"invalid_parameters","message":"Invalid request parameters.","details":[{"field":"limit","code":"too_big","message":"`limit` must be at most 200."}]}},"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"401":{"description":"The key is missing, unknown, revoked or expired, or the organization has switched the Integrations API off, and the response deliberately does not say which. Send a valid key as `Authorization: Bearer sk_tf_int_live_…`. If a key that worked before answers `401`, have an administrator check that it was not revoked or expired and that the API is still switched on.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"WWW-Authenticate":{"description":"Bearer authentication challenge returned on every 401.","required":true,"schema":{"type":"string","example":"Bearer"}}},"content":{"application/json":{"example":{"error":{"code":"unauthorized","message":"Missing or invalid API key. Send it as `Authorization: Bearer sk_tf_int_live_…`."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key is missing the `users:read` scope this endpoint requires. Scopes are fixed at key creation, so issue a new key carrying the scope named in the message.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"forbidden_scope","message":"This API key is missing the required scope `users:read`. Scopes are fixed at key creation — issue a new key to change them."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"The per-key budget for this window is spent. Failed-authentication throttling carries no per-key `X-RateLimit-*` headers. Wait for `Retry-After`, a delta in seconds, then retry.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"Retry-After":{"description":"Minimum delay before retrying, in seconds (a delta, not Unix epoch seconds).","required":true,"schema":{"type":"integer","minimum":1,"example":30}}},"content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"An unexpected error, with a `requestId` for support. Quote the `requestId` (or `X-Request-Id`) in a support request.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"internal_error","message":"An unexpected error occurred. Quote the request ID when contacting support.","requestId":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/integrations/v1/users/{id}":{"get":{"operationId":"getUser","tags":["People"],"summary":"Fetch one person by id","description":"Required scope: `users:read`. A single person by id. Deactivated people are returned and flagged, not hidden; an unknown id returns `404`.","x-required-scopes":["users:read"],"x-badges":[{"name":"users:read","position":"before"}],"x-scalar-stability":"stable","parameters":[{"schema":{"type":"string","format":"uuid","description":"Stable identifier of the person to retrieve."},"required":true,"description":"Stable identifier of the person to retrieve.","name":"id","in":"path"}],"responses":{"200":{"description":"The person.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"ETag":{"description":"Strong validator over this exact body. Send it back as `If-None-Match` on the next poll and an unchanged page answers `304` with no body. It covers the whole request — filters, cursor, and whether this key may see money — so tags from two different keys or two different queries never match.","required":true,"schema":{"type":"string","example":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\""}},"Cache-Control":{"description":"`no-cache` means revalidate before reuse, not do not store — it is what makes a conditional request happen. `private` keeps a body scoped to one API key out of shared caches.","required":true,"schema":{"type":"string","example":"private, no-cache"}},"Vary":{"description":"The response depends on the key that asked: two keys for the same organization can see different amounts on the same URL.","required":true,"schema":{"type":"string","example":"Authorization"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}},"304":{"description":"The representation is unchanged since the `ETag` in `If-None-Match`. No body. This still counts against the rate limit — the server ran the query and built the response; only the transfer was skipped.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"ETag":{"description":"Strong validator over this exact body. Send it back as `If-None-Match` on the next poll and an unchanged page answers `304` with no body. It covers the whole request — filters, cursor, and whether this key may see money — so tags from two different keys or two different queries never match.","required":true,"schema":{"type":"string","example":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\""}}}},"400":{"description":"The request was rejected before it ran. `error.code` says why:\n\n- `invalid_parameters` — a query, path or body value does not match the schema. Fix the fields `details[]` names.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"invalid_parameters","message":"Invalid request parameters.","details":[{"field":"limit","code":"too_big","message":"`limit` must be at most 200."}]}},"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"401":{"description":"The key is missing, unknown, revoked or expired, or the organization has switched the Integrations API off, and the response deliberately does not say which. Send a valid key as `Authorization: Bearer sk_tf_int_live_…`. If a key that worked before answers `401`, have an administrator check that it was not revoked or expired and that the API is still switched on.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"WWW-Authenticate":{"description":"Bearer authentication challenge returned on every 401.","required":true,"schema":{"type":"string","example":"Bearer"}}},"content":{"application/json":{"example":{"error":{"code":"unauthorized","message":"Missing or invalid API key. Send it as `Authorization: Bearer sk_tf_int_live_…`."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key is missing the `users:read` scope this endpoint requires. Scopes are fixed at key creation, so issue a new key carrying the scope named in the message.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"forbidden_scope","message":"This API key is missing the required scope `users:read`. Scopes are fixed at key creation — issue a new key to change them."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"User was not found.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"not_found","message":"User was not found."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"The per-key budget for this window is spent. Failed-authentication throttling carries no per-key `X-RateLimit-*` headers. Wait for `Retry-After`, a delta in seconds, then retry.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"Retry-After":{"description":"Minimum delay before retrying, in seconds (a delta, not Unix epoch seconds).","required":true,"schema":{"type":"integer","minimum":1,"example":30}}},"content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"An unexpected error, with a `requestId` for support. Quote the `requestId` (or `X-Request-Id`) in a support request.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"internal_error","message":"An unexpected error occurred. Quote the request ID when contacting support.","requestId":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/integrations/v1/rates/batch":{"post":{"operationId":"upsertRates","tags":["Hourly rates"],"summary":"Upsert hourly rates by externalId, in batch","description":"Required scope: `rates:write`. Upserts up to 100 hourly rates by `externalId`, each exactly as `PUT /rates/external/{externalId}` would, in order, in one transaction. All or nothing: when any item is refused, nothing is written and the `422` lists every refused item with the code it would have answered alone. Items run in order and each sees the ones before it.\n\n`adoptExisting` applies to every item. Each item may carry `ifMatch`, the single upsert's `If-Match`. `dryRun` runs everything and writes nothing. Safe to send again without a key: every item converges. An `Idempotency-Key` makes a retry answer with the original result instead.","x-required-scopes":["rates:write"],"x-badges":[{"name":"rates:write","position":"before"}],"x-scalar-stability":"stable","parameters":[{"schema":{"type":"string","pattern":"^[\\x20-\\x7e]{1,128}$","description":"Optional: 1–128 printable ASCII characters, case-sensitive. Send a unique key — a UUID is ideal — to make this write safely retryable: if you never see the response, send the **identical** request with the **same** key and the original result is returned instead of executing again. Keys are scoped to the organization and operation; any currently authorized API key in that organization may replay the result. Receipts are retained for 7 days from execution and describe that original execution, not the records' current state; a key without `money:read` is shown it without rates. A replay carries `Idempotency-Replayed: true`. The same key with a different body is `409 idempotency_key_reused`. Cannot be combined with `dryRun: true`. Without this header a retry is still safe, since every item is an upsert: it converges on the same records, answering `unchanged` where the first execution answered `created`.","example":"3f8c1b9e-2d47-4a1e-9a1b-5f6f0f7c2a10"},"required":false,"description":"Optional: 1–128 printable ASCII characters, case-sensitive. Send a unique key — a UUID is ideal — to make this write safely retryable: if you never see the response, send the **identical** request with the **same** key and the original result is returned instead of executing again. Keys are scoped to the organization and operation; any currently authorized API key in that organization may replay the result. Receipts are retained for 7 days from execution and describe that original execution, not the records' current state; a key without `money:read` is shown it without rates. A replay carries `Idempotency-Replayed: true`. The same key with a different body is `409 idempotency_key_reused`. Cannot be combined with `dryRun: true`. Without this header a retry is still safe, since every item is an upsert: it converges on the same records, answering `unchanged` where the first execution answered `created`.","name":"Idempotency-Key","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateBatchRequest"}}}},"responses":{"200":{"description":"Every item was written, or on a dry run would be. Check `dryRun` before recording the sync as done. With `Idempotency-Replayed: true`, this is the retained answer of an earlier execution.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"Idempotency-Replayed":{"description":"Sent as `true` when this response was replayed from a retained receipt rather than executed. Absent on a first execution. The body is the original one, so it describes that execution and not the records' current state.","schema":{"type":"string","example":"true"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateBatchResult"}}}},"400":{"description":"The request was rejected before it ran. `error.code` says why:\n\n- `malformed_json` — the request body is not valid JSON. Check the payload and the `Content-Type` header.\n- `invalid_parameters` — a query, path or body value does not match the schema. Fix the fields `details[]` names.\n- `idempotency_key_not_allowed` — an `Idempotency-Key` was sent with `dryRun: true`, and a rehearsal stores no receipt. Drop the key from the dry run and send it with the execution.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"invalid_parameters","message":"Invalid request parameters.","details":[{"field":"items.0.amount","code":"not_multiple_of","message":"`items.0.amount` must be a multiple of 0.01."}]}},"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"401":{"description":"The key is missing, unknown, revoked or expired, or the organization has switched the Integrations API off, and the response deliberately does not say which. Send a valid key as `Authorization: Bearer sk_tf_int_live_…`. If a key that worked before answers `401`, have an administrator check that it was not revoked or expired and that the API is still switched on.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"WWW-Authenticate":{"description":"Bearer authentication challenge returned on every 401.","required":true,"schema":{"type":"string","example":"Bearer"}}},"content":{"application/json":{"example":{"error":{"code":"unauthorized","message":"Missing or invalid API key. Send it as `Authorization: Bearer sk_tf_int_live_…`."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key is missing the `rates:write` scope this endpoint requires. Scopes are fixed at key creation, so issue a new key carrying the scope named in the message. Also returned as `money_unavailable` on every rate write while the key may not see money: without `money:read`, or with the organization's accounting feature switched off. `details[]` says which.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"forbidden_scope","message":"This API key is missing the required scope `rates:write`. Scopes are fixed at key creation — issue a new key to change them."}},"schema":{"anyOf":[{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/MoneyUnavailableError"}]}}}},"409":{"description":"The write was refused, so nothing was written. `error.code` says why:\n\n- `idempotency_key_reused` — this `Idempotency-Key` is retained against a different request body. Receipts are kept for 7 days. Send the original request unchanged, or a new key for a new intent.\n- `idempotency_request_in_progress` — another request holding this key is still running and did not finish within the bounded wait. **Retry** the identical request after a few seconds; it replays that request's result rather than executing twice.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"idempotency_key_reused","message":"This `Idempotency-Key` was already used for a different request body. Retry the original request unchanged, or generate a new key for the new request."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"One or more items of a master-data batch cannot be written. The batch is all-or-nothing, so nothing was written; `details[]` lists every refused item by `index` and `externalId`, with the code it would have answered alone.\n\nEach `details[]` entry carries the code that item would have answered alone, as the single upsert would:\n\n- `customer_name_taken` — another customer of the organization already has this name, compared case-insensitively; `details[]` carries its id. Nothing was written. When it is the customer you meant, link it: set your `externalId` on it with `PATCH /customers/{id}`, or send the upsert again with `?adoptExisting=true` (`adoptExisting: true` on a batch). When it is a different customer, choose another name.\n- `ambiguous_adoption` — `adoptExisting` found more than one unlinked record to adopt: projects with the body's `projectNumber`, or customers or hourly rates with the body's name, compared case-insensitively. `details[]` lists them. Nothing was written. Link the right record explicitly with `PATCH /projects/{id}`, `PATCH /customers/{id}` or `PATCH /rates/{id}` and its `externalId`.\n- `precondition_failed` — `If-Match` (a batch item's `ifMatch`) names a representation the record no longer has, or `If-Match: *` / `If-None-Match: *` disagree with whether the `externalId` is linked. Nothing was written. On a stale tag, re-read the record, reconcile the change, and send the write again with the new `ETag`. On `If-None-Match: *` the `externalId` is already linked, and on `If-Match: *` it is not: decide whether to update or create, and resend without the header.\n- `customer_not_found` — `customerId` or `customerExternalId` names no customer of this organization. Check the reference; when the customer is new, create it first.\n- `template_not_found` — `templateProjectId` names no project of this organization. Check the id, or create the project without a template.\n- `rate_not_found` — `rateId` or `rateExternalId` names no hourly rate of this organization. Check the reference against `GET /rates`; when the rate is new, create it first (`PUT /rates/external/{externalId}`).\n- `rate_archived` — `rateId` or `rateExternalId` names an archived rate, which cannot be newly assigned. A record already carrying it may keep it. Choose an active rate (`GET /rates?archived=false`).","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"batch_rejected","message":"1 of 3 items cannot be written, so nothing was written.","details":[{"index":2,"externalId":"K-1003","code":"customer_name_taken","message":"Another customer of this organization already has this name (compared case-insensitively); its id is in `details[]`. Link that customer if it is the one you meant, or choose another name.","ids":["88888888-8888-4888-8888-888888888888"]}]}},"schema":{"$ref":"#/components/schemas/BatchValidationError"}}}},"429":{"description":"The per-key budget for this window is spent. Failed-authentication throttling carries no per-key `X-RateLimit-*` headers. Wait for `Retry-After`, a delta in seconds, then retry.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"Retry-After":{"description":"Minimum delay before retrying, in seconds (a delta, not Unix epoch seconds).","required":true,"schema":{"type":"integer","minimum":1,"example":30}}},"content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"An unexpected error, with a `requestId` for support. Quote the `requestId` (or `X-Request-Id`) in a support request.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"internal_error","message":"An unexpected error occurred. Quote the request ID when contacting support.","requestId":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/integrations/v1/customers/batch":{"post":{"operationId":"upsertCustomers","tags":["Customers"],"summary":"Upsert customers by externalId, in batch","description":"Required scope: `customers:write`. Upserts up to 100 customers by `externalId`, each exactly as `PUT /customers/external/{externalId}` would, in order, in one transaction. All or nothing: when any item is refused, nothing is written and the `422` lists every refused item with the code it would have answered alone. Items run in order and each sees the ones before it.\n\n`adoptExisting` applies to every item. Each item may carry `ifMatch`, the single upsert's `If-Match`. `dryRun` runs everything and writes nothing. Safe to send again without a key: every item converges. An `Idempotency-Key` makes a retry answer with the original result instead.","x-required-scopes":["customers:write"],"x-badges":[{"name":"customers:write","position":"before"}],"x-scalar-stability":"stable","parameters":[{"schema":{"type":"string","pattern":"^[\\x20-\\x7e]{1,128}$","description":"Optional: 1–128 printable ASCII characters, case-sensitive. Send a unique key — a UUID is ideal — to make this write safely retryable: if you never see the response, send the **identical** request with the **same** key and the original result is returned instead of executing again. Keys are scoped to the organization and operation; any currently authorized API key in that organization may replay the result. Receipts are retained for 7 days from execution and describe that original execution, not the records' current state; a key without `money:read` is shown it without rates. A replay carries `Idempotency-Replayed: true`. The same key with a different body is `409 idempotency_key_reused`. Cannot be combined with `dryRun: true`. Without this header a retry is still safe, since every item is an upsert: it converges on the same records, answering `unchanged` where the first execution answered `created`.","example":"3f8c1b9e-2d47-4a1e-9a1b-5f6f0f7c2a10"},"required":false,"description":"Optional: 1–128 printable ASCII characters, case-sensitive. Send a unique key — a UUID is ideal — to make this write safely retryable: if you never see the response, send the **identical** request with the **same** key and the original result is returned instead of executing again. Keys are scoped to the organization and operation; any currently authorized API key in that organization may replay the result. Receipts are retained for 7 days from execution and describe that original execution, not the records' current state; a key without `money:read` is shown it without rates. A replay carries `Idempotency-Replayed: true`. The same key with a different body is `409 idempotency_key_reused`. Cannot be combined with `dryRun: true`. Without this header a retry is still safe, since every item is an upsert: it converges on the same records, answering `unchanged` where the first execution answered `created`.","name":"Idempotency-Key","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerBatchRequest"}}}},"responses":{"200":{"description":"Every item was written, or on a dry run would be. Check `dryRun` before recording the sync as done. With `Idempotency-Replayed: true`, this is the retained answer of an earlier execution.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"Idempotency-Replayed":{"description":"Sent as `true` when this response was replayed from a retained receipt rather than executed. Absent on a first execution. The body is the original one, so it describes that execution and not the records' current state.","schema":{"type":"string","example":"true"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerBatchResult"}}}},"400":{"description":"The request was rejected before it ran. `error.code` says why:\n\n- `malformed_json` — the request body is not valid JSON. Check the payload and the `Content-Type` header.\n- `invalid_parameters` — a query, path or body value does not match the schema. Fix the fields `details[]` names.\n- `idempotency_key_not_allowed` — an `Idempotency-Key` was sent with `dryRun: true`, and a rehearsal stores no receipt. Drop the key from the dry run and send it with the execution.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"invalid_parameters","message":"Invalid request parameters.","details":[{"field":"items.0.rateId","code":"invalid_format","message":"`items.0.rateId` must be a UUID."}]}},"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"401":{"description":"The key is missing, unknown, revoked or expired, or the organization has switched the Integrations API off, and the response deliberately does not say which. Send a valid key as `Authorization: Bearer sk_tf_int_live_…`. If a key that worked before answers `401`, have an administrator check that it was not revoked or expired and that the API is still switched on.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"WWW-Authenticate":{"description":"Bearer authentication challenge returned on every 401.","required":true,"schema":{"type":"string","example":"Bearer"}}},"content":{"application/json":{"example":{"error":{"code":"unauthorized","message":"Missing or invalid API key. Send it as `Authorization: Bearer sk_tf_int_live_…`."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key is missing the `customers:write` scope this endpoint requires. Scopes are fixed at key creation, so issue a new key carrying the scope named in the message. Also returned as `money_unavailable` when any item sends `rateId` or `rateExternalId` while the key may not see money: without `money:read`, or with the organization's accounting feature switched off. `details[]` names the first such item's field and says why.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"forbidden_scope","message":"This API key is missing the required scope `customers:write`. Scopes are fixed at key creation — issue a new key to change them."}},"schema":{"anyOf":[{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/MoneyUnavailableError"}]}}}},"409":{"description":"The write was refused, so nothing was written. `error.code` says why:\n\n- `idempotency_key_reused` — this `Idempotency-Key` is retained against a different request body. Receipts are kept for 7 days. Send the original request unchanged, or a new key for a new intent.\n- `idempotency_request_in_progress` — another request holding this key is still running and did not finish within the bounded wait. **Retry** the identical request after a few seconds; it replays that request's result rather than executing twice.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"idempotency_key_reused","message":"This `Idempotency-Key` was already used for a different request body. Retry the original request unchanged, or generate a new key for the new request."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"One or more items of a master-data batch cannot be written. The batch is all-or-nothing, so nothing was written; `details[]` lists every refused item by `index` and `externalId`, with the code it would have answered alone.\n\nEach `details[]` entry carries the code that item would have answered alone, as the single upsert would:\n\n- `customer_name_taken` — another customer of the organization already has this name, compared case-insensitively; `details[]` carries its id. Nothing was written. When it is the customer you meant, link it: set your `externalId` on it with `PATCH /customers/{id}`, or send the upsert again with `?adoptExisting=true` (`adoptExisting: true` on a batch). When it is a different customer, choose another name.\n- `ambiguous_adoption` — `adoptExisting` found more than one unlinked record to adopt: projects with the body's `projectNumber`, or customers or hourly rates with the body's name, compared case-insensitively. `details[]` lists them. Nothing was written. Link the right record explicitly with `PATCH /projects/{id}`, `PATCH /customers/{id}` or `PATCH /rates/{id}` and its `externalId`.\n- `precondition_failed` — `If-Match` (a batch item's `ifMatch`) names a representation the record no longer has, or `If-Match: *` / `If-None-Match: *` disagree with whether the `externalId` is linked. Nothing was written. On a stale tag, re-read the record, reconcile the change, and send the write again with the new `ETag`. On `If-None-Match: *` the `externalId` is already linked, and on `If-Match: *` it is not: decide whether to update or create, and resend without the header.\n- `customer_not_found` — `customerId` or `customerExternalId` names no customer of this organization. Check the reference; when the customer is new, create it first.\n- `template_not_found` — `templateProjectId` names no project of this organization. Check the id, or create the project without a template.\n- `rate_not_found` — `rateId` or `rateExternalId` names no hourly rate of this organization. Check the reference against `GET /rates`; when the rate is new, create it first (`PUT /rates/external/{externalId}`).\n- `rate_archived` — `rateId` or `rateExternalId` names an archived rate, which cannot be newly assigned. A record already carrying it may keep it. Choose an active rate (`GET /rates?archived=false`).","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"batch_rejected","message":"1 of 3 items cannot be written, so nothing was written.","details":[{"index":2,"externalId":"K-1003","code":"customer_name_taken","message":"Another customer of this organization already has this name (compared case-insensitively); its id is in `details[]`. Link that customer if it is the one you meant, or choose another name.","ids":["88888888-8888-4888-8888-888888888888"]}]}},"schema":{"$ref":"#/components/schemas/BatchValidationError"}}}},"429":{"description":"The per-key budget for this window is spent. Failed-authentication throttling carries no per-key `X-RateLimit-*` headers. Wait for `Retry-After`, a delta in seconds, then retry.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"Retry-After":{"description":"Minimum delay before retrying, in seconds (a delta, not Unix epoch seconds).","required":true,"schema":{"type":"integer","minimum":1,"example":30}}},"content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"An unexpected error, with a `requestId` for support. Quote the `requestId` (or `X-Request-Id`) in a support request.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"internal_error","message":"An unexpected error occurred. Quote the request ID when contacting support.","requestId":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/integrations/v1/projects/batch":{"post":{"operationId":"upsertProjects","tags":["Projects"],"summary":"Upsert projects by externalId, in batch","description":"Required scope: `projects:write`. Upserts up to 100 projects by `externalId`, each exactly as `PUT /projects/external/{externalId}` would, in order, in one transaction. All or nothing: when any item is refused, nothing is written and the `422` lists every refused item with the code it would have answered alone. Items run in order and each sees the ones before it. An item naming its customer by `customerExternalId` needs that customer linked already: send the customer batch first.\n\n`adoptExisting` applies to every item. Each item may carry `ifMatch`, the single upsert's `If-Match`. `dryRun` runs everything and writes nothing. Safe to send again without a key: every item converges. An `Idempotency-Key` makes a retry answer with the original result instead.","x-required-scopes":["projects:write"],"x-badges":[{"name":"projects:write","position":"before"}],"x-scalar-stability":"stable","parameters":[{"schema":{"type":"string","pattern":"^[\\x20-\\x7e]{1,128}$","description":"Optional: 1–128 printable ASCII characters, case-sensitive. Send a unique key — a UUID is ideal — to make this write safely retryable: if you never see the response, send the **identical** request with the **same** key and the original result is returned instead of executing again. Keys are scoped to the organization and operation; any currently authorized API key in that organization may replay the result. Receipts are retained for 7 days from execution and describe that original execution, not the records' current state; a key without `money:read` is shown it without rates. A replay carries `Idempotency-Replayed: true`. The same key with a different body is `409 idempotency_key_reused`. Cannot be combined with `dryRun: true`. Without this header a retry is still safe, since every item is an upsert: it converges on the same records, answering `unchanged` where the first execution answered `created`.","example":"3f8c1b9e-2d47-4a1e-9a1b-5f6f0f7c2a10"},"required":false,"description":"Optional: 1–128 printable ASCII characters, case-sensitive. Send a unique key — a UUID is ideal — to make this write safely retryable: if you never see the response, send the **identical** request with the **same** key and the original result is returned instead of executing again. Keys are scoped to the organization and operation; any currently authorized API key in that organization may replay the result. Receipts are retained for 7 days from execution and describe that original execution, not the records' current state; a key without `money:read` is shown it without rates. A replay carries `Idempotency-Replayed: true`. The same key with a different body is `409 idempotency_key_reused`. Cannot be combined with `dryRun: true`. Without this header a retry is still safe, since every item is an upsert: it converges on the same records, answering `unchanged` where the first execution answered `created`.","name":"Idempotency-Key","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectBatchRequest"}}}},"responses":{"200":{"description":"Every item was written, or on a dry run would be. Check `dryRun` before recording the sync as done. With `Idempotency-Replayed: true`, this is the retained answer of an earlier execution.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"Idempotency-Replayed":{"description":"Sent as `true` when this response was replayed from a retained receipt rather than executed. Absent on a first execution. The body is the original one, so it describes that execution and not the records' current state.","schema":{"type":"string","example":"true"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectBatchResult"}}}},"400":{"description":"The request was rejected before it ran. `error.code` says why:\n\n- `malformed_json` — the request body is not valid JSON. Check the payload and the `Content-Type` header.\n- `invalid_parameters` — a query, path or body value does not match the schema. Fix the fields `details[]` names.\n- `idempotency_key_not_allowed` — an `Idempotency-Key` was sent with `dryRun: true`, and a rehearsal stores no receipt. Drop the key from the dry run and send it with the execution.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"invalid_parameters","message":"Invalid request parameters.","details":[{"field":"items.0.rateId","code":"invalid_format","message":"`items.0.rateId` must be a UUID."}]}},"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"401":{"description":"The key is missing, unknown, revoked or expired, or the organization has switched the Integrations API off, and the response deliberately does not say which. Send a valid key as `Authorization: Bearer sk_tf_int_live_…`. If a key that worked before answers `401`, have an administrator check that it was not revoked or expired and that the API is still switched on.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"WWW-Authenticate":{"description":"Bearer authentication challenge returned on every 401.","required":true,"schema":{"type":"string","example":"Bearer"}}},"content":{"application/json":{"example":{"error":{"code":"unauthorized","message":"Missing or invalid API key. Send it as `Authorization: Bearer sk_tf_int_live_…`."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key is missing the `projects:write` scope this endpoint requires. Scopes are fixed at key creation, so issue a new key carrying the scope named in the message. Also returned as `money_unavailable` when any item sends `rateId` or `rateExternalId` while the key may not see money: without `money:read`, or with the organization's accounting feature switched off. `details[]` names the first such item's field and says why.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"forbidden_scope","message":"This API key is missing the required scope `projects:write`. Scopes are fixed at key creation — issue a new key to change them."}},"schema":{"anyOf":[{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/MoneyUnavailableError"}]}}}},"409":{"description":"The write was refused, so nothing was written. `error.code` says why:\n\n- `idempotency_key_reused` — this `Idempotency-Key` is retained against a different request body. Receipts are kept for 7 days. Send the original request unchanged, or a new key for a new intent.\n- `idempotency_request_in_progress` — another request holding this key is still running and did not finish within the bounded wait. **Retry** the identical request after a few seconds; it replays that request's result rather than executing twice.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"idempotency_key_reused","message":"This `Idempotency-Key` was already used for a different request body. Retry the original request unchanged, or generate a new key for the new request."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"One or more items of a master-data batch cannot be written. The batch is all-or-nothing, so nothing was written; `details[]` lists every refused item by `index` and `externalId`, with the code it would have answered alone.\n\nEach `details[]` entry carries the code that item would have answered alone, as the single upsert would:\n\n- `customer_name_taken` — another customer of the organization already has this name, compared case-insensitively; `details[]` carries its id. Nothing was written. When it is the customer you meant, link it: set your `externalId` on it with `PATCH /customers/{id}`, or send the upsert again with `?adoptExisting=true` (`adoptExisting: true` on a batch). When it is a different customer, choose another name.\n- `ambiguous_adoption` — `adoptExisting` found more than one unlinked record to adopt: projects with the body's `projectNumber`, or customers or hourly rates with the body's name, compared case-insensitively. `details[]` lists them. Nothing was written. Link the right record explicitly with `PATCH /projects/{id}`, `PATCH /customers/{id}` or `PATCH /rates/{id}` and its `externalId`.\n- `precondition_failed` — `If-Match` (a batch item's `ifMatch`) names a representation the record no longer has, or `If-Match: *` / `If-None-Match: *` disagree with whether the `externalId` is linked. Nothing was written. On a stale tag, re-read the record, reconcile the change, and send the write again with the new `ETag`. On `If-None-Match: *` the `externalId` is already linked, and on `If-Match: *` it is not: decide whether to update or create, and resend without the header.\n- `customer_not_found` — `customerId` or `customerExternalId` names no customer of this organization. Check the reference; when the customer is new, create it first.\n- `template_not_found` — `templateProjectId` names no project of this organization. Check the id, or create the project without a template.\n- `rate_not_found` — `rateId` or `rateExternalId` names no hourly rate of this organization. Check the reference against `GET /rates`; when the rate is new, create it first (`PUT /rates/external/{externalId}`).\n- `rate_archived` — `rateId` or `rateExternalId` names an archived rate, which cannot be newly assigned. A record already carrying it may keep it. Choose an active rate (`GET /rates?archived=false`).","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"batch_rejected","message":"1 of 2 items cannot be written, so nothing was written.","details":[{"index":1,"externalId":"P-2026-002","code":"customer_not_found","message":"`customerId` or `customerExternalId` names no customer of this organization."}]}},"schema":{"$ref":"#/components/schemas/BatchValidationError"}}}},"429":{"description":"The per-key budget for this window is spent. Failed-authentication throttling carries no per-key `X-RateLimit-*` headers. Wait for `Retry-After`, a delta in seconds, then retry.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"Retry-After":{"description":"Minimum delay before retrying, in seconds (a delta, not Unix epoch seconds).","required":true,"schema":{"type":"integer","minimum":1,"example":30}}},"content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"An unexpected error, with a `requestId` for support. Quote the `requestId` (or `X-Request-Id`) in a support request.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"internal_error","message":"An unexpected error occurred. Quote the request ID when contacting support.","requestId":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/integrations/v1/time-bookings/billing":{"post":{"operationId":"setTimeBookingsBilled","tags":["Billing & Invoices"],"summary":"Mark or unmark time bookings as billed","description":"Required scope: `billing:write`. Mark or unmark an explicit set of booking IDs in one transaction. A rejected booking rejects the whole batch. **Marking locks the bookings for their employees.** Use `dryRun` to rehearse and `Idempotency-Key` to make a retry safe. Three optional preconditions guard the gap between reviewing a selection and billing it, all checked against the write transaction's snapshot and all answering `409` `billing_precondition_failed`: `expectedBillingRevisions` refuses the write when a booking's billing inputs changed since the review, `requireFinalized` when a booking is not finalized, for example because its period was reopened, and `requireReadyForBilling` when a booking has no ready-for-billing mark. A mark refuses a booking whose analysis found an error or that has no analysis (`422`, `analysis_error` or `not_analyzed`). `expectedBillingRevisions` also needs the organization's accounting feature switched on (see `403`). See *Billing workflow* in the guide for selection, execution and recovery, and *Billing against reviewed state* for the preconditions.","x-required-scopes":["billing:write"],"x-badges":[{"name":"billing:write","position":"before"}],"x-scalar-stability":"stable","parameters":[{"schema":{"type":"string","pattern":"^[\\x20-\\x7e]{1,128}$","description":"Optional: 1–128 printable ASCII characters, case-sensitive. Send a unique key — a UUID is ideal — to make this write safely retryable: if you never see the response, send the **identical** request with the **same** key and the original result is returned instead of billing a second time. Keys are scoped to the organization and operation; any currently authorized API key in that organization may replay the result, even after the original API key is rotated or deleted. Receipts are retained for 7 days from execution and describe that original execution, not the bookings' current state. A replay carries `Idempotency-Replayed: true`. The same key with a different body is `409 idempotency_key_reused`; generate a new key when the intent changes. Cannot be combined with `dryRun: true`. Without this header nothing changes: identical marks still converge, but only absent intervening changes.","example":"3f8c1b9e-2d47-4a1e-9a1b-5f6f0f7c2a10"},"required":false,"description":"Optional: 1–128 printable ASCII characters, case-sensitive. Send a unique key — a UUID is ideal — to make this write safely retryable: if you never see the response, send the **identical** request with the **same** key and the original result is returned instead of billing a second time. Keys are scoped to the organization and operation; any currently authorized API key in that organization may replay the result, even after the original API key is rotated or deleted. Receipts are retained for 7 days from execution and describe that original execution, not the bookings' current state. A replay carries `Idempotency-Replayed: true`. The same key with a different body is `409 idempotency_key_reused`; generate a new key when the intent changes. Cannot be combined with `dryRun: true`. Without this header nothing changes: identical marks still converge, but only absent intervening changes.","name":"Idempotency-Key","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"examples":{"Rehearse (dry run)":{"summary":"Rehearse (dry run)","description":"Validates and reports, writes nothing. Start here.","value":{"bookingIds":["55555555-5555-4555-8555-555555555555"],"billed":true,"invoiceNumber":"RE-2026-0815","dryRun":true}},"Bill for real":{"summary":"Bill for real","description":"The same body with `dryRun: false`. Send it with an `Idempotency-Key` so a retry cannot bill twice.","value":{"bookingIds":["55555555-5555-4555-8555-555555555555"],"billed":true,"invoiceNumber":"RE-2026-0815","dryRun":false}},"Bill reviewed state":{"summary":"Bill reviewed state","description":"Adds the preconditions: `expectedBillingRevisions` refuses the write if a booking's billing inputs moved before the transaction snapshot, `requireFinalized` refuses it if a period was reopened before that snapshot. `requireFinalized` needs only `billing:write`; `expectedBillingRevisions` also needs `bookings:read`, `money:read` and the organization's accounting feature switched on.","value":{"bookingIds":["55555555-5555-4555-8555-555555555555"],"billed":true,"invoiceNumber":"RE-2026-0815","dryRun":false,"expectedBillingRevisions":{"55555555-5555-4555-8555-555555555555":"br1.k1.…"},"requireFinalized":true}},"Unmark":{"summary":"Unmark","description":"Releases the bookings again. No `invoiceNumber` — the current one is what gets cleared.","value":{"bookingIds":["55555555-5555-4555-8555-555555555555"],"billed":false,"dryRun":false}}},"schema":{"$ref":"#/components/schemas/BillingRequest"}}}},"responses":{"200":{"description":"The bookings are now in the requested state — or, on a dry run, would be. Check `dryRun` on the response before recording the run as done. A response carrying `Idempotency-Replayed: true` is a retained result of an earlier execution, not a fresh one.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"Idempotency-Replayed":{"description":"Sent as `true` when this response was replayed from a retained receipt rather than executed. Absent on a first execution. The body — including `changed` and `unchanged` — is the original one, so it describes that execution and not the bookings' current state.","schema":{"type":"string","example":"true"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BillingResult"}}}},"400":{"description":"The request was rejected before it ran. `error.code` says why:\n\n- `malformed_json` — the request body is not valid JSON. Check the payload and the `Content-Type` header.\n- `invalid_parameters` — a query, path or body value does not match the schema. Fix the fields `details[]` names.\n- `invoice_number_required` — `billed` is true without an `invoiceNumber`. Send an `invoiceNumber` with `billed: true`.\n- `invoice_number_not_allowed` — an `invoiceNumber` was sent with `billed: false`. Drop the `invoiceNumber` when unmarking.\n- `idempotency_key_not_allowed` — an `Idempotency-Key` was sent with `dryRun: true`, and a rehearsal stores no receipt. Drop the key from the dry run and send it with the execution.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"invalid_parameters","message":"Invalid request parameters.","details":[{"field":"bookingIds.0","code":"invalid_format","message":"`bookingIds.0` must be a UUID."}]}},"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"401":{"description":"The key is missing, unknown, revoked or expired, or the organization has switched the Integrations API off, and the response deliberately does not say which. Send a valid key as `Authorization: Bearer sk_tf_int_live_…`. If a key that worked before answers `401`, have an administrator check that it was not revoked or expired and that the API is still switched on.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"WWW-Authenticate":{"description":"Bearer authentication challenge returned on every 401.","required":true,"schema":{"type":"string","example":"Bearer"}}},"content":{"application/json":{"example":{"error":{"code":"unauthorized","message":"Missing or invalid API key. Send it as `Authorization: Bearer sk_tf_int_live_…`."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key is missing the `billing:write` scope this endpoint requires. Scopes are fixed at key creation, so issue a new key carrying the scope named in the message. Also returned when `expectedBillingRevisions` is sent while the organization's accounting feature is switched off. A new key does not help here: an administrator has to switch accounting back on.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"forbidden_scope","message":"This API key is missing the required scope `billing:write`. Scopes are fixed at key creation — issue a new key to change them."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"The request was refused after it was accepted for processing. `error.code` says why:\n\n- `billing_precondition_failed` — every booking was checked and one or more failed a review precondition; `details[]` names each as `stale_revision`, `not_finalized` or `not_ready_for_billing`. Nothing was written. Re-read and re-review the selection, reconcile any external invoice, then send a new request under a new key.\n- `conflict` — a concurrent change landed between validation and the write. Nothing was written. **Retry**, after re-reading the bookings.\n- `idempotency_key_reused` — this `Idempotency-Key` is retained against a different request body. Receipts are kept for 7 days. Send the original request unchanged, or a new key for a new intent.\n- `idempotency_request_in_progress` — another request holding this key is still running and did not finish within the bounded wait. **Retry** the identical request after a few seconds; it replays that request's result rather than executing twice.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"conflict","message":"The bookings changed while this request was being validated, so nothing was written. Re-read them and retry."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"The request repeats a booking id, or one or more bookings cannot be moved to the requested billing state. Nothing was written. Each `details[]` entry has an `id`, one of these codes, and the sentence beside it as its `message`:\n\n- `not_found`: No such booking.\n- `not_billable`: The booking is not marked billable, so it cannot be put on an invoice.\n- `not_completed`: The booking is still running and has no end time yet.\n- `not_analyzed`: The booking has no booking analysis yet, so it cannot be checked for errors. Retry later; the analysis is recalculated on demand.\n- `analysis_error`: The booking's analysis found an error, so it cannot be billed. The booking's `analysis.errorCode` names it; fix the booking first.\n- `already_billed_elsewhere`: The booking is already billed under a different invoice number. Unmark it first — it is never silently re-pointed.\n- `duplicate_id`: This booking id appears more than once in `bookingIds`.\n\nDuplicates are checked first; when present, only duplicate ids are reported. Otherwise every rejected id is listed. Unmarking checks existence but does not apply mark-time billability or completion rules.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"validation_failed","message":"1 of 3 bookings cannot be marked as billed.","details":[{"id":"55555555-5555-4555-8555-555555555555","code":"not_completed","message":"The booking is still running and has no end time yet."}]}},"schema":{"$ref":"#/components/schemas/BillingValidationError"}}}},"429":{"description":"The per-key budget for this window is spent. Failed-authentication throttling carries no per-key `X-RateLimit-*` headers. Wait for `Retry-After`, a delta in seconds, then retry.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"Retry-After":{"description":"Minimum delay before retrying, in seconds (a delta, not Unix epoch seconds).","required":true,"schema":{"type":"integer","minimum":1,"example":30}}},"content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"An unexpected error, with a `requestId` for support. Quote the `requestId` (or `X-Request-Id`) in a support request.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"internal_error","message":"An unexpected error occurred. Quote the request ID when contacting support.","requestId":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-codeSamples":[{"lang":"typescript","label":"TypeScript (fetch)","source":"// Rehearse the whole invoice, then execute it. At most 500 ids per\n// request and 20 writes/minute per key, so a large invoice is several requests.\nconst origin = process.env.TF_ORIGIN!;\nconst key = process.env.TF_KEY!;\n\n// One idempotency key per batch, generated and saved with the batch *before*\n// sending it, and reused verbatim on every retry of that batch. New intent -\n// different ids, a different invoice number - gets a new key.\nconst keys = Array.from(\n  { length: Math.ceil(bookingIds.length / 500) },\n  () => crypto.randomUUID(),\n);\n\nconst mark = async (bookingIds: string[], invoiceNumber: string, dryRun: boolean) => {\n  for (let from = 0; from < bookingIds.length; from += 500) {\n    const batch = bookingIds.slice(from, from + 500);\n    const response = await fetch(origin + \"/api/integrations/v1/time-bookings/billing\", {\n      method: \"POST\",\n      headers: {\n        Authorization: \"Bearer \" + key,\n        \"Content-Type\": \"application/json\",\n        // A dry run stores no receipt, so the header is a 400 there.\n        ...(dryRun ? {} : { \"Idempotency-Key\": keys[from / 500]! }),\n      },\n      // Unknown properties are rejected: a misspelled \"dryrun\" is a 400, never\n      // a silent real write.\n      body: JSON.stringify({ bookingIds: batch, billed: true, invoiceNumber, dryRun }),\n    });\n\n    const body = await response.json();\n    // A 422 names every rejected booking in details[] and wrote nothing at all.\n    if (!response.ok) throw new Error(response.status + \" \" + body.error.code);\n    // changed + unchanged === batch.length on every 200. A response carrying\n    // Idempotency-Replayed: true is the earlier execution's stored result, so it\n    // describes that execution rather than the bookings' state right now.\n    if (response.headers.get(\"Idempotency-Replayed\") === \"true\")\n      console.log(\"replayed - this batch was already billed, not billed twice\");\n    console.log(body.changed, \"changed,\", body.unchanged, \"unchanged\", body.dryRun ? \"(dry run)\" : \"\");\n  }\n};\n\nawait mark(bookingIds, \"RE-2026-0815\", true); // rehearsal: writes nothing\n// Safe to send again with the same keys after a timeout or a lost connection.\nawait mark(bookingIds, \"RE-2026-0815\", false); // a batch failing leaves earlier batches billed"}]}},"/api/integrations/v1/invoices":{"get":{"operationId":"listInvoices","tags":["Billing & Invoices"],"summary":"List invoices","description":"Required scope: `billing:read`. Oldest first by creation, in pages walked with `cursor`. Filter by `number` to answer \"did this invoice land?\" without walking the list, and by `from`/`to` to bound a poll to invoices created in a window. Time bookings and project expenses are counted and totaled **separately** (`timeBooking…` and `expense…`), so a difference against your own invoice total can be explained rather than investigated. They are not added together: your invoice may carry tax, discounts and lines TimeFusion never saw. To read the bookings themselves, `GET /time-bookings?invoiceNumber=…` needs no period.","x-required-scopes":["billing:read"],"x-badges":[{"name":"billing:read","position":"before"}],"x-scalar-stability":"stable","parameters":[{"schema":{"type":"array","items":{"type":"string","minLength":1},"minItems":1,"maxItems":20,"description":"Exact match on the invoice reference: \"did RE-2026-0815 land?\". A reference is unique within the organization, so each one matches at most one invoice and a request never needs a second page for this filter alone. The same strings filter `GET /time-bookings` as `invoiceNumber`. URL-encode the value, and send one occurrence per reference rather than joining them, since a reference may itself contain a comma. Repeat the parameter for several values — `?number=RE-2026-0815&number=RE-2026-0816` — and an invoice matches any of them; at most 20 per request. Repeated values within one parameter are ORed, different parameters are ANDed, and duplicates are ignored. Bracketed keys (`number[]`, `number[0]`) are rejected with `400`.","example":["RE-2026-0815","RE-2026-0816"]},"required":false,"description":"Exact match on the invoice reference: \"did RE-2026-0815 land?\". A reference is unique within the organization, so each one matches at most one invoice and a request never needs a second page for this filter alone. The same strings filter `GET /time-bookings` as `invoiceNumber`. URL-encode the value, and send one occurrence per reference rather than joining them, since a reference may itself contain a comma. Repeat the parameter for several values — `?number=RE-2026-0815&number=RE-2026-0816` — and an invoice matches any of them; at most 20 per request. Repeated values within one parameter are ORed, different parameters are ANDed, and duplicates are ignored. Bracketed keys (`number[]`, `number[0]`) are rejected with `400`.","name":"number","in":"query","style":"form","explode":true},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}([T ]\\d{2}:\\d{2}(:\\d{2}(\\.\\d{1,3})?)?(Z|[+-]\\d{2}:\\d{2})?)?$","description":"Inclusive start of a window on `createdAt`. Optional, unlike the required period on `GET /time-bookings`: omit both boundaries to list every invoice, as a first sync would. A date without a time means 00:00:00.000 UTC on that day. Timestamps without an offset are interpreted as UTC; explicit offsets are respected.","example":"2026-08-01"},"required":false,"description":"Inclusive start of a window on `createdAt`. Optional, unlike the required period on `GET /time-bookings`: omit both boundaries to list every invoice, as a first sync would. A date without a time means 00:00:00.000 UTC on that day. Timestamps without an offset are interpreted as UTC; explicit offsets are respected.","name":"from","in":"query"},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}([T ]\\d{2}:\\d{2}(:\\d{2}(\\.\\d{1,3})?)?(Z|[+-]\\d{2}:\\d{2})?)?$","description":"Inclusive end of a window on `createdAt`. A date without a time means 23:59:59.999 UTC on that day. Given together with `from`, the window may span at most 366 days; either boundary on its own is open-ended.","example":"2026-08-31"},"required":false,"description":"Inclusive end of a window on `createdAt`. A date without a time means 23:59:59.999 UTC on that day. Given together with `from`, the window may span at most 366 days; either boundary on its own is open-ended.","name":"to","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":200,"default":50,"description":"Page size, 1–200. Values above 200 are rejected, not clamped.","example":50},"required":false,"description":"Page size, 1–200. Values above 200 are rejected, not clamped.","name":"limit","in":"query"},{"schema":{"type":"string","description":"Opaque cursor from a previous page's `pagination.nextCursor` **on this endpoint**. Cursors are not interchangeable between endpoints; one issued elsewhere is rejected as `invalid_cursor`. Omit for the first page."},"required":false,"description":"Opaque cursor from a previous page's `pagination.nextCursor` **on this endpoint**. Cursors are not interchangeable between endpoints; one issued elsewhere is rejected as `invalid_cursor`. Omit for the first page.","name":"cursor","in":"query"}],"responses":{"200":{"description":"One page of invoices.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"ETag":{"description":"Strong validator over this exact body. Send it back as `If-None-Match` on the next poll and an unchanged page answers `304` with no body. It covers the whole request — filters, cursor, and whether this key may see money — so tags from two different keys or two different queries never match.","required":true,"schema":{"type":"string","example":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\""}},"Cache-Control":{"description":"`no-cache` means revalidate before reuse, not do not store — it is what makes a conditional request happen. `private` keeps a body scoped to one API key out of shared caches.","required":true,"schema":{"type":"string","example":"private, no-cache"}},"Vary":{"description":"The response depends on the key that asked: two keys for the same organization can see different amounts on the same URL.","required":true,"schema":{"type":"string","example":"Authorization"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvoicePage"}}}},"304":{"description":"The representation is unchanged since the `ETag` in `If-None-Match`. No body. This still counts against the rate limit — the server ran the query and built the response; only the transfer was skipped.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"ETag":{"description":"Strong validator over this exact body. Send it back as `If-None-Match` on the next poll and an unchanged page answers `304` with no body. It covers the whole request — filters, cursor, and whether this key may see money — so tags from two different keys or two different queries never match.","required":true,"schema":{"type":"string","example":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\""}}}},"400":{"description":"The request was rejected before it ran. `error.code` says why:\n\n- `invalid_parameters` — a query, path or body value does not match the schema. Fix the fields `details[]` names.\n- `invalid_period` — `from` or `to` is date-shaped but not a real date (`2026-13-01`, `2026-02-31`), or `to` is earlier than `from`. A boundary that is not date-shaped at all fails the schema as `invalid_parameters` instead. Send a real calendar date, with `to` no earlier than `from`.\n- `period_too_long` — the window is wider than the maximum span. Split the sync into narrower windows.\n- `invalid_cursor` — the `cursor` was not issued by this API. Discard the cursor and restart the walk from the first page.\n- `invoice_page_too_large` — the invoices this request addresses link more than 10000 bookings between them, which is more than amounts can be summed over. No partial totals are returned. Lower `limit` on the list; a single invoice over the bound has no `limit` to reduce and is totaled through `GET /time-bookings?invoiceNumber=…` instead.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"invalid_parameters","message":"Invalid request parameters.","details":[{"field":"limit","code":"too_big","message":"`limit` must be at most 200."}]}},"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"401":{"description":"The key is missing, unknown, revoked or expired, or the organization has switched the Integrations API off, and the response deliberately does not say which. Send a valid key as `Authorization: Bearer sk_tf_int_live_…`. If a key that worked before answers `401`, have an administrator check that it was not revoked or expired and that the API is still switched on.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"WWW-Authenticate":{"description":"Bearer authentication challenge returned on every 401.","required":true,"schema":{"type":"string","example":"Bearer"}}},"content":{"application/json":{"example":{"error":{"code":"unauthorized","message":"Missing or invalid API key. Send it as `Authorization: Bearer sk_tf_int_live_…`."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key is missing the `billing:read` scope this endpoint requires. Scopes are fixed at key creation, so issue a new key carrying the scope named in the message.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"forbidden_scope","message":"This API key is missing the required scope `billing:read`. Scopes are fixed at key creation — issue a new key to change them."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Bookings were billed onto the invoices this request addresses between the size check and the amount read, pushing them past the 10000 linked-booking limit. The request itself is valid. **Retry.** A request that is genuinely oversized then fails deterministically as `400 invoice_page_too_large`.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"invoice_page_changed","message":"Bookings were billed onto this invoice page while it was being read, pushing it past the 10000 linked-booking limit for amounts. Retry the request; if it keeps failing, reduce `limit`."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"The per-key budget for this window is spent. Failed-authentication throttling carries no per-key `X-RateLimit-*` headers. Wait for `Retry-After`, a delta in seconds, then retry.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"Retry-After":{"description":"Minimum delay before retrying, in seconds (a delta, not Unix epoch seconds).","required":true,"schema":{"type":"integer","minimum":1,"example":30}}},"content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"An unexpected error, with a `requestId` for support. Quote the `requestId` (or `X-Request-Id`) in a support request.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"internal_error","message":"An unexpected error occurred. Quote the request ID when contacting support.","requestId":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/integrations/v1/invoices/{id}":{"get":{"operationId":"getInvoice","tags":["Billing & Invoices"],"summary":"Fetch one invoice by id","description":"Required scope: `billing:read`. A single invoice by id. To find an invoice by your own reference rather than by id, use `GET /invoices?number=`. An unknown id returns `404`.","x-required-scopes":["billing:read"],"x-badges":[{"name":"billing:read","position":"before"}],"x-scalar-stability":"stable","parameters":[{"schema":{"type":"string","format":"uuid","description":"Stable identifier of the invoice to retrieve."},"required":true,"description":"Stable identifier of the invoice to retrieve.","name":"id","in":"path"}],"responses":{"200":{"description":"The invoice.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"ETag":{"description":"Strong validator over this exact body. Send it back as `If-None-Match` on the next poll and an unchanged page answers `304` with no body. It covers the whole request — filters, cursor, and whether this key may see money — so tags from two different keys or two different queries never match.","required":true,"schema":{"type":"string","example":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\""}},"Cache-Control":{"description":"`no-cache` means revalidate before reuse, not do not store — it is what makes a conditional request happen. `private` keeps a body scoped to one API key out of shared caches.","required":true,"schema":{"type":"string","example":"private, no-cache"}},"Vary":{"description":"The response depends on the key that asked: two keys for the same organization can see different amounts on the same URL.","required":true,"schema":{"type":"string","example":"Authorization"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Invoice"}}}},"304":{"description":"The representation is unchanged since the `ETag` in `If-None-Match`. No body. This still counts against the rate limit — the server ran the query and built the response; only the transfer was skipped.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"ETag":{"description":"Strong validator over this exact body. Send it back as `If-None-Match` on the next poll and an unchanged page answers `304` with no body. It covers the whole request — filters, cursor, and whether this key may see money — so tags from two different keys or two different queries never match.","required":true,"schema":{"type":"string","example":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\""}}}},"400":{"description":"The request was rejected before it ran. `error.code` says why:\n\n- `invalid_parameters` — a query, path or body value does not match the schema. Fix the fields `details[]` names.\n- `invoice_page_too_large` — the invoices this request addresses link more than 10000 bookings between them, which is more than amounts can be summed over. No partial totals are returned. Lower `limit` on the list; a single invoice over the bound has no `limit` to reduce and is totaled through `GET /time-bookings?invoiceNumber=…` instead.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"invalid_parameters","message":"Invalid request parameters.","details":[{"field":"limit","code":"too_big","message":"`limit` must be at most 200."}]}},"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"401":{"description":"The key is missing, unknown, revoked or expired, or the organization has switched the Integrations API off, and the response deliberately does not say which. Send a valid key as `Authorization: Bearer sk_tf_int_live_…`. If a key that worked before answers `401`, have an administrator check that it was not revoked or expired and that the API is still switched on.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"WWW-Authenticate":{"description":"Bearer authentication challenge returned on every 401.","required":true,"schema":{"type":"string","example":"Bearer"}}},"content":{"application/json":{"example":{"error":{"code":"unauthorized","message":"Missing or invalid API key. Send it as `Authorization: Bearer sk_tf_int_live_…`."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key is missing the `billing:read` scope this endpoint requires. Scopes are fixed at key creation, so issue a new key carrying the scope named in the message.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"forbidden_scope","message":"This API key is missing the required scope `billing:read`. Scopes are fixed at key creation — issue a new key to change them."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Invoice was not found.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"not_found","message":"Invoice was not found."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Bookings were billed onto the invoices this request addresses between the size check and the amount read, pushing them past the 10000 linked-booking limit. The request itself is valid. **Retry.** A request that is genuinely oversized then fails deterministically as `400 invoice_page_too_large`.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"invoice_page_changed","message":"Bookings were billed onto this invoice page while it was being read, pushing it past the 10000 linked-booking limit for amounts. Retry the request; if it keeps failing, reduce `limit`."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"The per-key budget for this window is spent. Failed-authentication throttling carries no per-key `X-RateLimit-*` headers. Wait for `Retry-After`, a delta in seconds, then retry.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}},"Retry-After":{"description":"Minimum delay before retrying, in seconds (a delta, not Unix epoch seconds).","required":true,"schema":{"type":"integer","minimum":1,"example":30}}},"content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"An unexpected error, with a `requestId` for support. Quote the `requestId` (or `X-Request-Id`) in a support request.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"X-RateLimit-Limit":{"description":"Per-key requests allowed per sliding 60-second window: 120 reads or 20 writes. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":1,"example":120}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current per-key window after this request. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":119}},"X-RateLimit-Reset":{"description":"Per-key window reset as Unix epoch seconds, not a delay. Absent if the per-key limiter did not run or was unavailable.","schema":{"type":"integer","minimum":0,"example":1786692660}}},"content":{"application/json":{"example":{"error":{"code":"internal_error","message":"An unexpected error occurred. Quote the request ID when contacting support.","requestId":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/integrations/v1/openapi.json":{"get":{"operationId":"getOpenApiDocument","tags":["Documentation"],"summary":"Fetch the OpenAPI document","description":"Required scope: none. This endpoint is public: it takes no key at all. The machine-readable contract, OpenAPI 3.2, generated from the same schemas that validate every request.","security":[],"x-required-scopes":[],"x-badges":[{"name":"no key","position":"before"}],"x-scalar-stability":"stable","responses":{"200":{"description":"The OpenAPI 3.2 document.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"Cache-Control":{"description":"Public documentation may be cached for five minutes. `X-Request-Id` is excluded from the directive, so a shared cache serves no other caller's correlation ID from a stored copy.","required":true,"schema":{"type":"string","example":"public, max-age=300, no-cache=\"X-Request-Id\""}},"ETag":{"description":"Strong validator over this exact document. Send it back as `If-None-Match` and an unchanged contract answers `304` with no body — worth doing on every run, since the document changes only when the API does.","required":true,"schema":{"type":"string","example":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\""}},"Content-Disposition":{"description":"Always `attachment`, with the name the contract is saved under. Every download of it uses this name, whichever control was used.","required":true,"schema":{"type":"string","example":"attachment; filename=\"timefusion-integrations-api-spec-openapi-3.2-v1.2.0.json\""}}},"content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":{},"description":"An OpenAPI 3.2 document."}}}},"304":{"description":"The document is unchanged since the `ETag` in `If-None-Match`. No body.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"Cache-Control":{"description":"Public documentation may be cached for five minutes. `X-Request-Id` is excluded from the directive, so a shared cache serves no other caller's correlation ID from a stored copy.","required":true,"schema":{"type":"string","example":"public, max-age=300, no-cache=\"X-Request-Id\""}},"ETag":{"description":"Strong validator over this exact document. Send it back as `If-None-Match` and an unchanged contract answers `304` with no body — worth doing on every run, since the document changes only when the API does.","required":true,"schema":{"type":"string","example":"\"Q1Rn8Ke2v0YyH3sJ7pL4mXt6bWc\""}}}},"429":{"description":"The budget for this window is spent. This endpoint takes no API key, so the counter behind it is keyed by the caller's address and shared with everything else behind it: it is not the per-key budget of §8, and none of the `X-RateLimit-*` headers that describe that one appear here. Wait for `Retry-After`, a delta in seconds, then retry.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"Retry-After":{"description":"Minimum delay before retrying, in seconds (a delta, not Unix epoch seconds).","required":true,"schema":{"type":"integer","minimum":1,"example":30}},"Cache-Control":{"description":"Always `no-store`: this response is specific to one caller, on routes whose successful responses are publicly cacheable.","required":true,"schema":{"type":"string","example":"no-store"}}},"content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"An unexpected error, with a `requestId` for support. Quote the `requestId` (or `X-Request-Id`) in a support request.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}}},"content":{"application/json":{"example":{"error":{"code":"internal_error","message":"An unexpected error occurred. Quote the request ID when contacting support.","requestId":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/integrations/v1/docs":{"get":{"operationId":"getApiReferencePage","tags":["Documentation"],"summary":"Open the API reference","description":"Required scope: none. This endpoint is public: it takes no key at all. Renders `openapi.json` as a browsable reference.","security":[],"x-required-scopes":[],"x-badges":[{"name":"no key","position":"before"}],"x-scalar-stability":"stable","responses":{"200":{"description":"The reference page.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"Cache-Control":{"description":"Public documentation may be cached for five minutes. `X-Request-Id` is excluded from the directive, so a shared cache serves no other caller's correlation ID from a stored copy.","required":true,"schema":{"type":"string","example":"public, max-age=300, no-cache=\"X-Request-Id\""}},"ETag":{"description":"Strong validator over this exact page. Send it back as `If-None-Match` and an unchanged page answers `304` with no body; the page changes only when the API or its rendering does.","required":true,"schema":{"type":"string","example":"\"7Yb2Tq9wR0nK5vL3xM8dJc1\""}}},"content":{"text/html":{"schema":{"type":"string"}}}},"304":{"description":"The page is unchanged since the `ETag` in `If-None-Match`. No body.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"Cache-Control":{"description":"Public documentation may be cached for five minutes. `X-Request-Id` is excluded from the directive, so a shared cache serves no other caller's correlation ID from a stored copy.","required":true,"schema":{"type":"string","example":"public, max-age=300, no-cache=\"X-Request-Id\""}},"ETag":{"description":"Strong validator over this exact page. Send it back as `If-None-Match` and an unchanged page answers `304` with no body; the page changes only when the API or its rendering does.","required":true,"schema":{"type":"string","example":"\"7Yb2Tq9wR0nK5vL3xM8dJc1\""}}}},"429":{"description":"The budget for this window is spent. This endpoint takes no API key, so the counter behind it is keyed by the caller's address and shared with everything else behind it: it is not the per-key budget of §8, and none of the `X-RateLimit-*` headers that describe that one appear here. Wait for `Retry-After`, a delta in seconds, then retry.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"Retry-After":{"description":"Minimum delay before retrying, in seconds (a delta, not Unix epoch seconds).","required":true,"schema":{"type":"integer","minimum":1,"example":30}},"Cache-Control":{"description":"Always `no-store`: this response is specific to one caller, on routes whose successful responses are publicly cacheable.","required":true,"schema":{"type":"string","example":"no-store"}}},"content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"An unexpected error, with a `requestId` for support. Quote the `requestId` (or `X-Request-Id`) in a support request.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}}},"content":{"application/json":{"example":{"error":{"code":"internal_error","message":"An unexpected error occurred. Quote the request ID when contacting support.","requestId":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/integrations/v1/starter-kit.tar.gz":{"get":{"operationId":"getStarterKit","tags":["Documentation"],"summary":"Download the starter kit","description":"Required scope: none. This endpoint is public: it takes no key at all. A runnable Node.js starter kit for this API version, to run on your backend, as a `.tar.gz`: the six sources `client.ts`, `shared.ts`, `check.ts`, `billing.ts`, `sync.ts` and `push.ts`, a package manifest, an environment template, an example of the file `push.ts` reads, and a README. It belongs to v1 and changes only together with it; a later API version publishes its own starter kit under its own path.","security":[],"x-required-scopes":[],"x-badges":[{"name":"no key","position":"before"}],"x-scalar-stability":"stable","responses":{"200":{"description":"A gzip-compressed tar archive with one directory, `timefusion-integrations-starter-kit`.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"Cache-Control":{"description":"Public documentation may be cached for five minutes. `X-Request-Id` is excluded from the directive, so a shared cache serves no other caller's correlation ID from a stored copy.","required":true,"schema":{"type":"string","example":"public, max-age=300, no-cache=\"X-Request-Id\""}},"ETag":{"description":"Strong validator over this exact file. Send it back as `If-None-Match` and an unchanged file answers `304` with no body; the file changes only together with this API version.","required":true,"schema":{"type":"string","example":"\"3pVw8Hn1Kc6Zq0Rt5Ly2Ja9\""}},"Content-Disposition":{"description":"Always `attachment`, with the file name to save it under.","required":true,"schema":{"type":"string","example":"attachment; filename=\"timefusion-integrations-starter-kit.tar.gz\""}}},"content":{"application/gzip":{"schema":{"type":"string","contentMediaType":"application/gzip"}}}},"304":{"description":"The file is unchanged since the `ETag` in `If-None-Match`. No body.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"Cache-Control":{"description":"Public documentation may be cached for five minutes. `X-Request-Id` is excluded from the directive, so a shared cache serves no other caller's correlation ID from a stored copy.","required":true,"schema":{"type":"string","example":"public, max-age=300, no-cache=\"X-Request-Id\""}},"ETag":{"description":"Strong validator over this exact file. Send it back as `If-None-Match` and an unchanged file answers `304` with no body; the file changes only together with this API version.","required":true,"schema":{"type":"string","example":"\"3pVw8Hn1Kc6Zq0Rt5Ly2Ja9\""}}}},"429":{"description":"The budget for this window is spent. This endpoint takes no API key, so the counter behind it is keyed by the caller's address and shared with everything else behind it: it is not the per-key budget of §8, and none of the `X-RateLimit-*` headers that describe that one appear here. Wait for `Retry-After`, a delta in seconds, then retry.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"Retry-After":{"description":"Minimum delay before retrying, in seconds (a delta, not Unix epoch seconds).","required":true,"schema":{"type":"integer","minimum":1,"example":30}},"Cache-Control":{"description":"Always `no-store`: this response is specific to one caller, on routes whose successful responses are publicly cacheable.","required":true,"schema":{"type":"string","example":"no-store"}}},"content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"An unexpected error, with a `requestId` for support. Quote the `requestId` (or `X-Request-Id`) in a support request.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}}},"content":{"application/json":{"example":{"error":{"code":"internal_error","message":"An unexpected error occurred. Quote the request ID when contacting support.","requestId":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/integrations/v1/starter-kit/client.ts":{"get":{"operationId":"getStarterKitClient","tags":["Documentation"],"summary":"Download starter kit file: client.ts","description":"Required scope: none. This endpoint is public: it takes no key at all. The API client, and the one file to copy into your own backend: no dependencies, a typed method for every operation, pagination, retries, conditional requests, and the sync, billing and push workflows the commands run. Also inside the archive; downloaded alone, Node 24 runs it as it is, beside the other five sources.","security":[],"x-required-scopes":[],"x-badges":[{"name":"no key","position":"before"}],"x-scalar-stability":"stable","responses":{"200":{"description":"The TypeScript source, as UTF-8 text.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"Cache-Control":{"description":"Public documentation may be cached for five minutes. `X-Request-Id` is excluded from the directive, so a shared cache serves no other caller's correlation ID from a stored copy.","required":true,"schema":{"type":"string","example":"public, max-age=300, no-cache=\"X-Request-Id\""}},"ETag":{"description":"Strong validator over this exact file. Send it back as `If-None-Match` and an unchanged file answers `304` with no body; the file changes only together with this API version.","required":true,"schema":{"type":"string","example":"\"3pVw8Hn1Kc6Zq0Rt5Ly2Ja9\""}},"Content-Disposition":{"description":"Always `attachment`, with the file name to save it under.","required":true,"schema":{"type":"string","example":"attachment; filename=\"timefusion-integrations-starter-kit.tar.gz\""}}},"content":{"text/plain":{"schema":{"type":"string"}}}},"304":{"description":"The file is unchanged since the `ETag` in `If-None-Match`. No body.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"Cache-Control":{"description":"Public documentation may be cached for five minutes. `X-Request-Id` is excluded from the directive, so a shared cache serves no other caller's correlation ID from a stored copy.","required":true,"schema":{"type":"string","example":"public, max-age=300, no-cache=\"X-Request-Id\""}},"ETag":{"description":"Strong validator over this exact file. Send it back as `If-None-Match` and an unchanged file answers `304` with no body; the file changes only together with this API version.","required":true,"schema":{"type":"string","example":"\"3pVw8Hn1Kc6Zq0Rt5Ly2Ja9\""}}}},"429":{"description":"The budget for this window is spent. This endpoint takes no API key, so the counter behind it is keyed by the caller's address and shared with everything else behind it: it is not the per-key budget of §8, and none of the `X-RateLimit-*` headers that describe that one appear here. Wait for `Retry-After`, a delta in seconds, then retry.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"Retry-After":{"description":"Minimum delay before retrying, in seconds (a delta, not Unix epoch seconds).","required":true,"schema":{"type":"integer","minimum":1,"example":30}},"Cache-Control":{"description":"Always `no-store`: this response is specific to one caller, on routes whose successful responses are publicly cacheable.","required":true,"schema":{"type":"string","example":"no-store"}}},"content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"An unexpected error, with a `requestId` for support. Quote the `requestId` (or `X-Request-Id`) in a support request.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}}},"content":{"application/json":{"example":{"error":{"code":"internal_error","message":"An unexpected error occurred. Quote the request ID when contacting support.","requestId":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/integrations/v1/starter-kit/shared.ts":{"get":{"operationId":"getStarterKitShared","tags":["Documentation"],"summary":"Download starter kit file: shared.ts","description":"Required scope: none. This endpoint is public: it takes no key at all. What the four commands share: settings from `.env` and the startup check of the key's scopes. Not needed when you use the client in your own code. Also inside the archive; downloaded alone, Node 24 runs it as it is, beside the other five sources.","security":[],"x-required-scopes":[],"x-badges":[{"name":"no key","position":"before"}],"x-scalar-stability":"stable","responses":{"200":{"description":"The TypeScript source, as UTF-8 text.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"Cache-Control":{"description":"Public documentation may be cached for five minutes. `X-Request-Id` is excluded from the directive, so a shared cache serves no other caller's correlation ID from a stored copy.","required":true,"schema":{"type":"string","example":"public, max-age=300, no-cache=\"X-Request-Id\""}},"ETag":{"description":"Strong validator over this exact file. Send it back as `If-None-Match` and an unchanged file answers `304` with no body; the file changes only together with this API version.","required":true,"schema":{"type":"string","example":"\"3pVw8Hn1Kc6Zq0Rt5Ly2Ja9\""}},"Content-Disposition":{"description":"Always `attachment`, with the file name to save it under.","required":true,"schema":{"type":"string","example":"attachment; filename=\"timefusion-integrations-starter-kit.tar.gz\""}}},"content":{"text/plain":{"schema":{"type":"string"}}}},"304":{"description":"The file is unchanged since the `ETag` in `If-None-Match`. No body.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"Cache-Control":{"description":"Public documentation may be cached for five minutes. `X-Request-Id` is excluded from the directive, so a shared cache serves no other caller's correlation ID from a stored copy.","required":true,"schema":{"type":"string","example":"public, max-age=300, no-cache=\"X-Request-Id\""}},"ETag":{"description":"Strong validator over this exact file. Send it back as `If-None-Match` and an unchanged file answers `304` with no body; the file changes only together with this API version.","required":true,"schema":{"type":"string","example":"\"3pVw8Hn1Kc6Zq0Rt5Ly2Ja9\""}}}},"429":{"description":"The budget for this window is spent. This endpoint takes no API key, so the counter behind it is keyed by the caller's address and shared with everything else behind it: it is not the per-key budget of §8, and none of the `X-RateLimit-*` headers that describe that one appear here. Wait for `Retry-After`, a delta in seconds, then retry.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"Retry-After":{"description":"Minimum delay before retrying, in seconds (a delta, not Unix epoch seconds).","required":true,"schema":{"type":"integer","minimum":1,"example":30}},"Cache-Control":{"description":"Always `no-store`: this response is specific to one caller, on routes whose successful responses are publicly cacheable.","required":true,"schema":{"type":"string","example":"no-store"}}},"content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"An unexpected error, with a `requestId` for support. Quote the `requestId` (or `X-Request-Id`) in a support request.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}}},"content":{"application/json":{"example":{"error":{"code":"internal_error","message":"An unexpected error occurred. Quote the request ID when contacting support.","requestId":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/integrations/v1/starter-kit/check.ts":{"get":{"operationId":"getStarterKitCheck","tags":["Documentation"],"summary":"Download starter kit file: check.ts","description":"Required scope: none. This endpoint is public: it takes no key at all. The connection check: which organization the key belongs to, its scopes, the time zone and currency, and whether money is visible. Also inside the archive; downloaded alone, Node 24 runs it as it is, beside the other five sources.","security":[],"x-required-scopes":[],"x-badges":[{"name":"no key","position":"before"}],"x-scalar-stability":"stable","responses":{"200":{"description":"The TypeScript source, as UTF-8 text.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"Cache-Control":{"description":"Public documentation may be cached for five minutes. `X-Request-Id` is excluded from the directive, so a shared cache serves no other caller's correlation ID from a stored copy.","required":true,"schema":{"type":"string","example":"public, max-age=300, no-cache=\"X-Request-Id\""}},"ETag":{"description":"Strong validator over this exact file. Send it back as `If-None-Match` and an unchanged file answers `304` with no body; the file changes only together with this API version.","required":true,"schema":{"type":"string","example":"\"3pVw8Hn1Kc6Zq0Rt5Ly2Ja9\""}},"Content-Disposition":{"description":"Always `attachment`, with the file name to save it under.","required":true,"schema":{"type":"string","example":"attachment; filename=\"timefusion-integrations-starter-kit.tar.gz\""}}},"content":{"text/plain":{"schema":{"type":"string"}}}},"304":{"description":"The file is unchanged since the `ETag` in `If-None-Match`. No body.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"Cache-Control":{"description":"Public documentation may be cached for five minutes. `X-Request-Id` is excluded from the directive, so a shared cache serves no other caller's correlation ID from a stored copy.","required":true,"schema":{"type":"string","example":"public, max-age=300, no-cache=\"X-Request-Id\""}},"ETag":{"description":"Strong validator over this exact file. Send it back as `If-None-Match` and an unchanged file answers `304` with no body; the file changes only together with this API version.","required":true,"schema":{"type":"string","example":"\"3pVw8Hn1Kc6Zq0Rt5Ly2Ja9\""}}}},"429":{"description":"The budget for this window is spent. This endpoint takes no API key, so the counter behind it is keyed by the caller's address and shared with everything else behind it: it is not the per-key budget of §8, and none of the `X-RateLimit-*` headers that describe that one appear here. Wait for `Retry-After`, a delta in seconds, then retry.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"Retry-After":{"description":"Minimum delay before retrying, in seconds (a delta, not Unix epoch seconds).","required":true,"schema":{"type":"integer","minimum":1,"example":30}},"Cache-Control":{"description":"Always `no-store`: this response is specific to one caller, on routes whose successful responses are publicly cacheable.","required":true,"schema":{"type":"string","example":"no-store"}}},"content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"An unexpected error, with a `requestId` for support. Quote the `requestId` (or `X-Request-Id`) in a support request.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}}},"content":{"application/json":{"example":{"error":{"code":"internal_error","message":"An unexpected error occurred. Quote the request ID when contacting support.","requestId":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/integrations/v1/starter-kit/billing.ts":{"get":{"operationId":"getStarterKitBilling","tags":["Documentation"],"summary":"Download starter kit file: billing.ts","description":"Required scope: none. This endpoint is public: it takes no key at all. The billing run: plan a manifest, rehearse it with `dryRun`, and execute it with an `Idempotency-Key`. Also inside the archive; downloaded alone, Node 24 runs it as it is, beside the other five sources.","security":[],"x-required-scopes":[],"x-badges":[{"name":"no key","position":"before"}],"x-scalar-stability":"stable","responses":{"200":{"description":"The TypeScript source, as UTF-8 text.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"Cache-Control":{"description":"Public documentation may be cached for five minutes. `X-Request-Id` is excluded from the directive, so a shared cache serves no other caller's correlation ID from a stored copy.","required":true,"schema":{"type":"string","example":"public, max-age=300, no-cache=\"X-Request-Id\""}},"ETag":{"description":"Strong validator over this exact file. Send it back as `If-None-Match` and an unchanged file answers `304` with no body; the file changes only together with this API version.","required":true,"schema":{"type":"string","example":"\"3pVw8Hn1Kc6Zq0Rt5Ly2Ja9\""}},"Content-Disposition":{"description":"Always `attachment`, with the file name to save it under.","required":true,"schema":{"type":"string","example":"attachment; filename=\"timefusion-integrations-starter-kit.tar.gz\""}}},"content":{"text/plain":{"schema":{"type":"string"}}}},"304":{"description":"The file is unchanged since the `ETag` in `If-None-Match`. No body.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"Cache-Control":{"description":"Public documentation may be cached for five minutes. `X-Request-Id` is excluded from the directive, so a shared cache serves no other caller's correlation ID from a stored copy.","required":true,"schema":{"type":"string","example":"public, max-age=300, no-cache=\"X-Request-Id\""}},"ETag":{"description":"Strong validator over this exact file. Send it back as `If-None-Match` and an unchanged file answers `304` with no body; the file changes only together with this API version.","required":true,"schema":{"type":"string","example":"\"3pVw8Hn1Kc6Zq0Rt5Ly2Ja9\""}}}},"429":{"description":"The budget for this window is spent. This endpoint takes no API key, so the counter behind it is keyed by the caller's address and shared with everything else behind it: it is not the per-key budget of §8, and none of the `X-RateLimit-*` headers that describe that one appear here. Wait for `Retry-After`, a delta in seconds, then retry.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"Retry-After":{"description":"Minimum delay before retrying, in seconds (a delta, not Unix epoch seconds).","required":true,"schema":{"type":"integer","minimum":1,"example":30}},"Cache-Control":{"description":"Always `no-store`: this response is specific to one caller, on routes whose successful responses are publicly cacheable.","required":true,"schema":{"type":"string","example":"no-store"}}},"content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"An unexpected error, with a `requestId` for support. Quote the `requestId` (or `X-Request-Id`) in a support request.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}}},"content":{"application/json":{"example":{"error":{"code":"internal_error","message":"An unexpected error occurred. Quote the request ID when contacting support.","requestId":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/integrations/v1/starter-kit/sync.ts":{"get":{"operationId":"getStarterKitSync","tags":["Documentation"],"summary":"Download starter kit file: sync.ts","description":"Required scope: none. This endpoint is public: it takes no key at all. The read-only export: a full re-read of the period, or an incremental walk from a saved checkpoint, into a local mirror file. Also inside the archive; downloaded alone, Node 24 runs it as it is, beside the other five sources.","security":[],"x-required-scopes":[],"x-badges":[{"name":"no key","position":"before"}],"x-scalar-stability":"stable","responses":{"200":{"description":"The TypeScript source, as UTF-8 text.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"Cache-Control":{"description":"Public documentation may be cached for five minutes. `X-Request-Id` is excluded from the directive, so a shared cache serves no other caller's correlation ID from a stored copy.","required":true,"schema":{"type":"string","example":"public, max-age=300, no-cache=\"X-Request-Id\""}},"ETag":{"description":"Strong validator over this exact file. Send it back as `If-None-Match` and an unchanged file answers `304` with no body; the file changes only together with this API version.","required":true,"schema":{"type":"string","example":"\"3pVw8Hn1Kc6Zq0Rt5Ly2Ja9\""}},"Content-Disposition":{"description":"Always `attachment`, with the file name to save it under.","required":true,"schema":{"type":"string","example":"attachment; filename=\"timefusion-integrations-starter-kit.tar.gz\""}}},"content":{"text/plain":{"schema":{"type":"string"}}}},"304":{"description":"The file is unchanged since the `ETag` in `If-None-Match`. No body.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"Cache-Control":{"description":"Public documentation may be cached for five minutes. `X-Request-Id` is excluded from the directive, so a shared cache serves no other caller's correlation ID from a stored copy.","required":true,"schema":{"type":"string","example":"public, max-age=300, no-cache=\"X-Request-Id\""}},"ETag":{"description":"Strong validator over this exact file. Send it back as `If-None-Match` and an unchanged file answers `304` with no body; the file changes only together with this API version.","required":true,"schema":{"type":"string","example":"\"3pVw8Hn1Kc6Zq0Rt5Ly2Ja9\""}}}},"429":{"description":"The budget for this window is spent. This endpoint takes no API key, so the counter behind it is keyed by the caller's address and shared with everything else behind it: it is not the per-key budget of §8, and none of the `X-RateLimit-*` headers that describe that one appear here. Wait for `Retry-After`, a delta in seconds, then retry.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"Retry-After":{"description":"Minimum delay before retrying, in seconds (a delta, not Unix epoch seconds).","required":true,"schema":{"type":"integer","minimum":1,"example":30}},"Cache-Control":{"description":"Always `no-store`: this response is specific to one caller, on routes whose successful responses are publicly cacheable.","required":true,"schema":{"type":"string","example":"no-store"}}},"content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"An unexpected error, with a `requestId` for support. Quote the `requestId` (or `X-Request-Id`) in a support request.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}}},"content":{"application/json":{"example":{"error":{"code":"internal_error","message":"An unexpected error occurred. Quote the request ID when contacting support.","requestId":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/integrations/v1/starter-kit/push.ts":{"get":{"operationId":"getStarterKitPush","tags":["Documentation"],"summary":"Download starter kit file: push.ts","description":"Required scope: none. This endpoint is public: it takes no key at all. The master-data push: customers, then projects, from a JSON file of your system's records, upserted in batches by `externalId`; rehearsed with `dryRun` unless run with `execute`. Also inside the archive; downloaded alone, Node 24 runs it as it is, beside the other five sources.","security":[],"x-required-scopes":[],"x-badges":[{"name":"no key","position":"before"}],"x-scalar-stability":"stable","responses":{"200":{"description":"The TypeScript source, as UTF-8 text.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"Cache-Control":{"description":"Public documentation may be cached for five minutes. `X-Request-Id` is excluded from the directive, so a shared cache serves no other caller's correlation ID from a stored copy.","required":true,"schema":{"type":"string","example":"public, max-age=300, no-cache=\"X-Request-Id\""}},"ETag":{"description":"Strong validator over this exact file. Send it back as `If-None-Match` and an unchanged file answers `304` with no body; the file changes only together with this API version.","required":true,"schema":{"type":"string","example":"\"3pVw8Hn1Kc6Zq0Rt5Ly2Ja9\""}},"Content-Disposition":{"description":"Always `attachment`, with the file name to save it under.","required":true,"schema":{"type":"string","example":"attachment; filename=\"timefusion-integrations-starter-kit.tar.gz\""}}},"content":{"text/plain":{"schema":{"type":"string"}}}},"304":{"description":"The file is unchanged since the `ETag` in `If-None-Match`. No body.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"Cache-Control":{"description":"Public documentation may be cached for five minutes. `X-Request-Id` is excluded from the directive, so a shared cache serves no other caller's correlation ID from a stored copy.","required":true,"schema":{"type":"string","example":"public, max-age=300, no-cache=\"X-Request-Id\""}},"ETag":{"description":"Strong validator over this exact file. Send it back as `If-None-Match` and an unchanged file answers `304` with no body; the file changes only together with this API version.","required":true,"schema":{"type":"string","example":"\"3pVw8Hn1Kc6Zq0Rt5Ly2Ja9\""}}}},"429":{"description":"The budget for this window is spent. This endpoint takes no API key, so the counter behind it is keyed by the caller's address and shared with everything else behind it: it is not the per-key budget of §8, and none of the `X-RateLimit-*` headers that describe that one appear here. Wait for `Retry-After`, a delta in seconds, then retry.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}},"Retry-After":{"description":"Minimum delay before retrying, in seconds (a delta, not Unix epoch seconds).","required":true,"schema":{"type":"integer","minimum":1,"example":30}},"Cache-Control":{"description":"Always `no-store`: this response is specific to one caller, on routes whose successful responses are publicly cacheable.","required":true,"schema":{"type":"string","example":"no-store"}}},"content":{"application/json":{"example":{"error":{"code":"rate_limited","message":"Rate limit exceeded. Retry after the number of seconds in the `Retry-After` header."}},"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"An unexpected error, with a `requestId` for support. Quote the `requestId` (or `X-Request-Id`) in a support request.","headers":{"X-Request-Id":{"description":"Correlation ID. A valid inbound ID is echoed; otherwise one is generated. Quote it when contacting support.","required":true,"schema":{"type":"string","example":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"X-Api-Version":{"description":"The `info.version` of the contract that answered. Compare it with the version your client was written against: any difference, patch releases included, means the contract changed; see the changelog.","required":true,"schema":{"type":"string","example":"1.2.0"}}},"content":{"application/json":{"example":{"error":{"code":"internal_error","message":"An unexpected error occurred. Quote the request ID when contacting support.","requestId":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"}},"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"webhooks":{}}