> ## Documentation Index
> Fetch the complete documentation index at: https://sponson.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# API verification

> How the fake cloud's assumptions are checked against the real provider APIs.

Sponson's adapters (`packages/adapters/src/`) and the fake cloud (`packages/sim/src/routes/`) were written
without real provider accounts. This page records what was checked, without an account, against each provider's
published API specification: every HTTP call an adapter makes, and every numbered assumption at the top of a sim
routes file. What a specification cannot settle still needs a live run of `pnpm test:live`
(`scenarios/contract.test.ts`).

Legend: ✅ matches the spec · ⚠️ differed from the spec (fixed in adapter and sim unless noted) · ❓ the spec is
silent; needs a live account.

## Sources

All fetched on 2026-10-10 (LaunchDarkly, PlanetScale, Supabase, Netlify: 2026-10-11) and treated as data; none is vendored into the repository.

| Provider | Source | Version |
| - | - | - |
| Vercel | OpenAPI document, [https://openapi.vercel.sh/](https://openapi.vercel.sh/) | as served on that date |
| Neon | OpenAPI document, [https://neon.tech/api\_spec/release/v2.json](https://neon.tech/api_spec/release/v2.json) | `v2` |
| Clerk | Backend API OpenAPI, [https://github.com/clerk/openapi-specs](https://github.com/clerk/openapi-specs) `bapi/2026-05-12.yml` (commit `3b078e5`); the `/redirect_urls` section is identical in `bapi/2021-02-05.yml` | `2026-05-12` |
| Clerk | Official SDK sources, for the list envelope the spec does not describe: RedirectUrlApi.ts (backend package) in clerk/javascript, redirecturl/client.go in clerk/clerk-sdk-go (v2) | `main` / `v2` on that date |
| LaunchDarkly | REST API reference: overview (authentication, errors, rate limits, semantic patch) [https://launchdarkly.com/docs/api](https://launchdarkly.com/docs/api), "Get feature flag" [https://launchdarkly.com/docs/api/feature-flags/get-feature-flag](https://launchdarkly.com/docs/api/feature-flags/get-feature-flag), "Update feature flag" [https://launchdarkly.com/docs/api/feature-flags/patch-feature-flag](https://launchdarkly.com/docs/api/feature-flags/patch-feature-flag) | as served on that date (API `v2`) |
| PlanetScale | OpenAPI document, [https://planetscale.com/docs/openapi.yaml](https://planetscale.com/docs/openapi.yaml) (fetched 2026-10-11); service tokens, [https://planetscale.com/docs/api/reference/service-tokens](https://planetscale.com/docs/api/reference/service-tokens); CLI references [https://planetscale.com/docs/cli/service-tokens.md](https://planetscale.com/docs/cli/service-tokens.md), [https://planetscale.com/docs/cli/branch.md](https://planetscale.com/docs/cli/branch.md), [https://planetscale.com/docs/cli/password.md](https://planetscale.com/docs/cli/password.md); Node.js connection string, [https://planetscale.com/docs/vitess/tutorials/connect-nodejs-app](https://planetscale.com/docs/vitess/tutorials/connect-nodejs-app) | `v1` |
| Supabase | Management API OpenAPI document, [https://api.supabase.com/api/v1-json](https://api.supabase.com/api/v1-json) (rendered at [https://supabase.com/docs/reference/api/introduction](https://supabase.com/docs/reference/api/introduction)) | `1.0.0` as served on that date |
| Supabase | Docs pages, for what the spec does not say: Auth's `URI_ALLOW_LIST` format ([https://github.com/supabase/auth](https://github.com/supabase/auth), README "General Config"), the direct connection string ([https://supabase.com/docs/guides/database/connecting-to-postgres](https://supabase.com/docs/guides/database/connecting-to-postgres)), the project API URL ([https://supabase.com/docs/guides/api](https://supabase.com/docs/guides/api)), Branching ([https://supabase.com/docs/guides/deployment/branching](https://supabase.com/docs/guides/deployment/branching)), redirect URL wildcards ([https://supabase.com/docs/guides/auth/redirect-urls](https://supabase.com/docs/guides/auth/redirect-urls)) | as served on that date |
| Netlify | OpenAPI document, [https://github.com/netlify/open-api/blob/master/swagger.yml](https://github.com/netlify/open-api/blob/master/swagger.yml) (rendered at [https://open-api.netlify.com/](https://open-api.netlify.com/)), fetched 2026-10-11 | `master` on that date |
| Netlify | Docs, fetched 2026-10-11: API guide [https://docs.netlify.com/api-and-cli-guides/api-guides/get-started-with-api/](https://docs.netlify.com/api-and-cli-guides/api-guides/get-started-with-api/), env vars [https://docs.netlify.com/build/environment-variables/overview/](https://docs.netlify.com/build/environment-variables/overview/) and …/get-started/, Secrets Controller [https://docs.netlify.com/build/environment-variables/secrets-controller/](https://docs.netlify.com/build/environment-variables/secrets-controller/), deploy overview [https://docs.netlify.com/deploy/deploy-overview/](https://docs.netlify.com/deploy/deploy-overview/) | as served on that date |
| Cloudflare | OpenAPI document, [https://github.com/cloudflare/api-schemas](https://github.com/cloudflare/api-schemas) (`openapi.json`); API reference [https://developers.cloudflare.com/api/resources/pages/subresources/projects/methods/get/](https://developers.cloudflare.com/api/resources/pages/subresources/projects/methods/get/) and [`…/methods/edit/`](https://developers.cloudflare.com/api/resources/pages/subresources/projects/methods/edit/); the Pages and fundamentals pages cited below | fetched 2026-10-11 |
| Algolia | Search API reference [https://www.algolia.com/doc/rest-api/search/](https://www.algolia.com/doc/rest-api/search/), generated from [https://github.com/algolia/api-clients-automation](https://github.com/algolia/api-clients-automation) (`specs/search`: `spec.yml`, `operationIndex.yml`, `getTask.yml`, `settings.yml`, `listIndices.yml`); copy guide [https://www.algolia.com/doc/guides/sending-and-managing-data/manage-indices-and-apps/manage-indices/how-to/copy-indices](https://www.algolia.com/doc/guides/sending-and-managing-data/manage-indices-and-apps/manage-indices/how-to/copy-indices); CLI authentication [https://www.algolia.com/doc/tools/cli/authentication](https://www.algolia.com/doc/tools/cli/authentication) | fetched 2026-10-11 |
| Xata | OpenAPI document [https://api.xata.tech/openapi.json](https://api.xata.tech/openapi.json); docs [https://xata.io/docs/cli](https://xata.io/docs/cli), [https://xata.io/docs/cli/branch](https://xata.io/docs/cli/branch), [https://xata.io/docs/platform/branch](https://xata.io/docs/platform/branch) | fetched 2026-10-11 |
| Contentful | CMA reference [https://www.contentful.com/developers/docs/references/content-management-api/](https://www.contentful.com/developers/docs/references/content-management-api/) (environments, environment states, API keys, versioning) and [https://www.contentful.com/developers/docs/technical-limits/](https://www.contentful.com/developers/docs/technical-limits/), read through the Wayback Machine (contentful.com answers automated fetches with 429); official SDK source [https://github.com/contentful/contentful-management.js](https://github.com/contentful/contentful-management.js) (`lib/adapters/REST/endpoints/environment.ts`, `api-key.ts`, `rest-adapter.ts`), recorded API traffic in [https://github.com/contentful/contentful-management.py](https://github.com/contentful/contentful-management.py) (`fixtures/environment/*.yaml`, `fixtures/api_key/*.yaml`), CLI [https://github.com/contentful/contentful-cli](https://github.com/contentful/contentful-cli) (`environment create --await-processing`) | fetched 2026-10-12 |
| DigitalOcean | OpenAPI document [https://github.com/digitalocean/openapi](https://github.com/digitalocean/openapi) (`specification/resources/apps`: `apps_create.yml`, `apps_get.yml`, `apps_update.yml`, `apps_delete.yml`, `apps_list.yml`, `apps_get_deployment.yml`, `models/app.yml`, `models/app_spec.yml`, `models/app_variable_definition.yml`, `models/apps_deployment_phase.yml`), rendered at [https://docs.digitalocean.com/reference/api/digitalocean/#tag/Apps](https://docs.digitalocean.com/reference/api/digitalocean/#tag/Apps); app spec reference [https://docs.digitalocean.com/products/app-platform/reference/app-spec/](https://docs.digitalocean.com/products/app-platform/reference/app-spec/) | fetched 2026-10-11 |
| Google Cloud Run | Discovery document [https://run.googleapis.com/\$discovery/rest?version=v2](https://run.googleapis.com/\$discovery/rest?version=v2) (Service, RevisionTemplate, TrafficTarget, TrafficTargetStatus, Revision, Condition, Operation, Status; `services.patch`, `services.revisions.get`, `operations.get`); REST reference [https://docs.cloud.google.com/run/docs/reference/rest/v2/projects.locations.services](https://docs.cloud.google.com/run/docs/reference/rest/v2/projects.locations.services) and [`…/patch`](https://docs.cloud.google.com/run/docs/reference/rest/v2/projects.locations.services/patch); docs [https://docs.cloud.google.com/run/docs/rollouts-rollbacks-traffic-migration](https://docs.cloud.google.com/run/docs/rollouts-rollbacks-traffic-migration), [https://docs.cloud.google.com/run/docs/configuring/services/containers](https://docs.cloud.google.com/run/docs/configuring/services/containers), [https://docs.cloud.google.com/run/docs/managing/revisions](https://docs.cloud.google.com/run/docs/managing/revisions), [https://docs.cloud.google.com/run/docs/triggering/https-request](https://docs.cloud.google.com/run/docs/triggering/https-request), [https://docs.cloud.google.com/run/quotas](https://docs.cloud.google.com/run/quotas); gcloud references [https://docs.cloud.google.com/sdk/gcloud/reference/run/deploy](https://docs.cloud.google.com/sdk/gcloud/reference/run/deploy) and [https://docs.cloud.google.com/sdk/gcloud/reference/auth/print-access-token](https://docs.cloud.google.com/sdk/gcloud/reference/auth/print-access-token) | fetched 2026-10-11 |
| HashiCorp Vault | KV v2 API [https://developer.hashicorp.com/vault/api-docs/secret/kv/kv-v2](https://developer.hashicorp.com/vault/api-docs/secret/kv/kv-v2) ("Read secret version"); HTTP API overview (token header, namespaces, status codes, error format) [https://developer.hashicorp.com/vault/api-docs](https://developer.hashicorp.com/vault/api-docs) | fetched 2026-10-11 |
| Infisical | CLI reference [https://infisical.com/docs/cli/commands/secrets](https://infisical.com/docs/cli/commands/secrets); CLI source [https://github.com/Infisical/cli/blob/main/packages/cmd/secrets.go](https://github.com/Infisical/cli/blob/main/packages/cmd/secrets.go) (`getSecretsByNames`), [https://github.com/Infisical/cli/blob/main/packages/cmd/root.go](https://github.com/Infisical/cli/blob/main/packages/cmd/root.go), [https://github.com/Infisical/cli/blob/main/packages/util/common.go](https://github.com/Infisical/cli/blob/main/packages/util/common.go) (`FormatOutput`) | `main` on 2026-10-11 |
| Azure Key Vault | CLI reference [https://learn.microsoft.com/en-us/cli/azure/keyvault/secret#az-keyvault-secret-show](https://learn.microsoft.com/en-us/cli/azure/keyvault/secret#az-keyvault-secret-show); naming rules [https://learn.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates](https://learn.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates) | fetched 2026-10-11 |
| 1Password Connect | Connect API reference [https://www.1password.dev/connect/api-reference/](https://www.1password.dev/connect/api-reference/) (formerly developer.1password.com/docs/connect/api-reference); Connect with the CLI [https://developer.1password.com/docs/connect/connect-cli](https://developer.1password.com/docs/connect/connect-cli); Go SDK README [https://github.com/1Password/connect-sdk-go](https://github.com/1Password/connect-sdk-go) | fetched 2026-10-11 |
| Bitwarden Secrets Manager | CLI docs [https://bitwarden.com/help/secrets-manager-cli/](https://bitwarden.com/help/secrets-manager-cli/); CLI source [https://github.com/bitwarden/sdk-sm](https://github.com/bitwarden/sdk-sm) (`crates/bws/src/cli.rs`, `command/secret.rs`, `render.rs`) and [https://github.com/bitwarden/sdk-internal](https://github.com/bitwarden/sdk-internal) (`crates/bitwarden-cli/src/color.rs`) | `main` on 2026-10-11 |

Vercel, Neon and Clerk use bearer authentication (`securitySchemes`), which is what the shared HTTP client sends
by default: ✅. LaunchDarkly takes the access token as the whole `Authorization` value, without `Bearer`; the adapter
sends it that way (`authHeader: "header:authorization"`): ✅. PlanetScale service tokens are not bearer tokens
either; see below. Supabase's Management API and Netlify's API use bearer tokens: ✅. Cloudflare's API token is a bearer token
too ("The preferred authorization scheme"): ✅.

## Vercel

### Calls the adapter makes

| Call | What the adapter sends and reads | Spec | Notes |
| - | - | - | - |
| `GET /v10/projects/{idOrName}/env` | `teamId`; reads `envs[].id`, `key`, `value`, `target` (string or list), `gitBranch`, `type`, `decrypted` | ⚠️ → ✅ | Was `/v9/…`, which the spec no longer lists. `decrypt` is listed but deprecated, and checked live on 2026-10-11 it no longer decrypts (`decrypted: false`, an opaque value; V3), so the adapter no longer sends it: a value with `decrypted` not `true` is fetched from the call below instead of being hashed as ciphertext. |
| same, pagination | follows `pagination.next` as `?until=` | ❓ | The response has the spec's `Pagination` object (`count`, `next`, `prev`), but this endpoint documents no page parameter. The adapter drops repeated records, so an API that ignores `until` costs one request, not duplicates. |
| `GET /v1/projects/{idOrName}/env/{id}` | `teamId`; reads `value`, `decrypted`, `type` | ✅ (live ✅) | New. "Retrieve the decrypted value of an environment variable"; live on 2026-10-11 it answered `decrypted: true` with the plaintext, the only way to read an `encrypted` value. A `sensitive` value is never returned (the response variant without `value`). |
| `POST /v10/projects/{idOrName}/env` | `upsert=true`, `teamId`; body: list of `{ key, value, type: "encrypted", target: [t], gitBranch? }` | ✅ | Body matches the array form of the request schema. |
| same, answer | `201 { created, failed }`; `created` one object or a list; `failed[].error` | ⚠️ → ✅ (live ✅) | The failure names the variable as `key` or `envVarKey`; the adapter read only `key`. The sim answered `200`; the spec says `201`. Live on 2026-10-11, an upsert of an existing key and target answered `201` with `created` a list holding the same id, `createdAt` unchanged, a newer `updatedAt`, and `failed: []` (V4). |
| same, `gitBranch` without a Git connection | `400 { error: { code: "BAD_REQUEST", message: "Project \"<name>\" does not have a connected Git repository.", key: "project", envVarKey, project } }` → `PARAM_INVALID` | live ✅ | Not in the spec; found live on 2026-10-11 (V9). Branch-bound preview values (the default for `target: preview`) need a project connected to a repository; the adapter's message names both fixes: connect it, or `branch: "*"` for project-wide values. |
| same, conflicts | an overlapping target set refused with `400 ENV_CONFLICT` | ❓ | Not described. Without `upsert`, a duplicate is documented as `403` ("cannot be created because it already exists"); the sim now answers that. A `400` for "an ongoing env update is already happening" is documented; it is reported as `PROVIDER_INVALID`, not retried. |
| `DELETE /v9/projects/{idOrName}/env/{id}` | `teamId`; `404` is success | ✅ | `409` (project being transferred) surfaces as `PROVIDER_CONFLICT`. |
| `GET /v7/deployments` | `projectId`, `sha`, `target`, `limit=20`, `teamId`; reads `uid`, `url`, `readyState`, `createdAt`, `target` | ⚠️ → ✅ | Was `/v6/deployments?sha=`: `sha` is a documented filter of `/v7` only, and v6 is not in the spec. The adapter required `state`, which is optional; `readyState` is the required field. `url` is null until an upload completes, which made the whole list a shape error. Lookups were also by commit only, so a production line could take the commit's preview build; they now pass the documented `target` filter and re-check each item's `target` (null = preview), see V8. |
| deployment states | `READY` done; `ERROR`, `CANCELED` failed; others in progress | ⚠️ → ✅ | The spec's states add `INITIALIZING` (in progress), `BLOCKED` and `DELETED`; the last two never become ready by themselves and now fail the waiting lines. |
| `GET /v9/projects/{idOrName}` | `teamId`; reads `link.type`, `link.repoId` / `projectId` / `uuid`, `workspaceUuid` | ✅ | New, to build `gitSource` (next row). |
| `POST /v13/deployments` | `forceNew=1`, `teamId`; body `{ name, project, gitSource, target? }` | ⚠️ → ✅ | `gitSource` was `{ type: "github", ref, sha }`; every GitHub variant of the schema also requires `repoId` (or `org` and `repo`), so the call would have been refused. It now carries the connected repository from the project's `link` (GitHub, GitLab, Bitbucket); a project with no Git connection is `PROVIDER_INVALID`. `project` overriding `name` matches the spec. `forceNew=1` because Vercel may otherwise answer with an earlier deployment of the same commit, which defeats a redeploy. |
| same, answer | reads `id`, `readyState`, `url`, `createdAt` | ⚠️ → ✅ | One documented answer variant has no `url`; the adapter now polls until `url` is set. |
| `GET /v13/deployments/{idOrUrl}` | `teamId`; reads `id`, `url`, `readyState` | ✅ | |

### Sim assumptions (`packages/sim/src/routes/vercel.ts`)

| Id | Assumption | Status |
| - | - | - |
| V1 | env writes visible to the next deployment and list at once | ✅ verified live 2026-10-11 for the list (a variable written with `?upsert=true` is in the next GET list); a deployment's view not checked separately (pinned by the contract suite) |
| V2 | `GET /v7/deployments?sha=` filters by commit, newest first | `sha` filter verified; order ❓ (the adapter sorts anyway) |
| V8 | `GET /v7/deployments?target=` filters by environment; items carry `target` (`production`, a custom environment, or null for previews) | ✅ verified (parameter and item schema) |
| V3 | the list never decrypts an `encrypted` value (even with `decrypt=true`); GET `/v1/projects/{id}/env/{id}` does; `sensitive` values never returned | ⚠️ contradicted live 2026-10-11 (the sim assumed `decrypt=true` decrypts; it answers `decrypted: false` and an opaque value): sim fixed, the adapter no longer sends `decrypt`; the per-variable GET verified live (pinned) |
| V4 | upsert answer `{ created, failed }`; `ENV_CONFLICT` on overlapping targets | answer shape and status verified; `created` listing an update with the same id, `createdAt` kept and a newer `updatedAt` ✅ verified live 2026-10-11 (pinned); `ENV_CONFLICT` ❓ |
| V5 | `project` overrides `name`; `gitSource` identity; states; one URL per deployment | verified, except auto-cancel of a branch's older builds ❓ |
| V6 | `{ envs, pagination: { count, next, prev } }`, `?until=` | envelope verified; the page parameter ❓ |
| V7 | `GET /v9/projects/{id}` carries `link` | verified |
| V9 | a `gitBranch` value on a project with no connected Git repository answers `400 BAD_REQUEST` ("does not have a connected Git repository") and writes nothing | ✅ verified live 2026-10-11 (new; pinned) |

## Neon

### Calls the adapter makes

| Call | What the adapter sends and reads | Spec | Notes |
| - | - | - | - |
| `GET /projects/{project_id}/branches` | `cursor` from `pagination.next`; reads `branches[].id`, `name`, `parent_id`, `default` (and the deprecated `primary`) | ✅ | `CursorParam`, `CursorPagination`. A paged read starts with `limit`; the adapter sends none and follows `next` if present. |
| `POST /projects/{project_id}/branches` | `{ branch: { name, parent_id }, endpoints: [{ type: "read_write" }] }` | ✅ | Matches the spec's own `branch_with_endpoint` example. |
| same, answer | `201`; reads `branch`, `endpoints[].host`, `connection_uris[].connection_uri` | ✅ | `connection_uris` is absent when the parent has several databases or roles; the adapter then reads them with the calls below. |
| same, refusals | `409` duplicate name → re-read and claim; `423` retried | `423` ✅, `409` live ✅ | The generic error says `423 Locked` is always safe to retry. A duplicate name is not documented; live on 2026-10-11 it answered `409 { code: "BRANCH_ALREADY_EXISTS", message: "branch already exists; branch_name:…" }` (N5). |
| same, `parent` omitted | forks from the branch listed `default: true` | live ✅ | Was `main` by default. A new Neon project's root branch is called `production` (checked live 2026-10-11, N6), so a first apply with the default failed; the default branch is now found by its flag, whatever its name. |
| `GET /projects/{project_id}/branches/{branch_id}/endpoints` | reads `endpoints[].host`, preferring `type: read_write` | ✅ | |
| `GET /projects/{project_id}/branches/{branch_id}/databases` | reads `databases[].name`, `owner_name` | ✅ | New (next row). |
| `GET /projects/{project_id}/connection_uri` | `branch_id`, `database_name`, `role_name`; reads `uri` | ⚠️ → ✅ | Both names are required parameters. They were hard-coded to `neondb` / `neondb_owner`, a new project's defaults, so any other project failed to read. They now come from the branch's database list (`neondb` if present, else the first; its owner is the role). |
| `DELETE /projects/{project_id}/branches/{branch_id}` | `404` is success | ✅ path and status; timing live ✅ | "The deletion completes after all operations finish": not synchronous. Live on 2026-10-11, right after DELETE answered `200` (operations still running or scheduling) the branch was already absent from the list (N1), which is all the adapter relies on. |

The spec also says a `503` is always safe to retry; the shared HTTP client retries `5xx` only for idempotent
methods, which is more cautious than needed but not wrong.

### Sim assumptions (`packages/sim/src/routes/neon.ts`)

| Id | Assumption | Status |
| - | - | - |
| N1 | a deleted branch is gone from the list when DELETE returns | ✅ verified live 2026-10-11 (absent from the list at once, while the delete's operations still run; pinned) |
| N2 | a create runs operations; endpoint usable at once; `423` only as the opt-in chaos fault `neon_op_ms` | ⚠️ contradicted in part live 2026-10-11: a second create in the same project right after a create (operations running) answered `201`, not `423`; the sim no longer locks creates by default. `423` retry-safety verified (the adapter retries it); endpoint readiness ❓ (pinned) |
| N3 | `neondb` / `neondb_owner`; `default: true` on the root branch | the adapter no longer depends on the names (verified calls above); `default` verified |
| N4 | `{ branches, pagination: { next } }`, `?cursor=` | verified |
| N5 | a duplicate branch name answers `409 { code: "BRANCH_ALREADY_EXISTS", message: "branch already exists; branch_name:…" }` | ✅ verified live 2026-10-11 (sim body aligned; pinned) |
| N6 | a new project's root branch is `production` and listed `default: true` | ✅ verified live 2026-10-11 (new; the adapter forks from `default` when `parent` is omitted; pinned) |

## Clerk

### Calls the adapter makes

| Call | What the adapter sends and reads | Spec | Notes |
| - | - | - | - |
| `GET /redirect_urls` | `paginated=true`, `limit=100`, `offset`; reads `data[].id`, `url`, `total_count` (a bare array also accepted) | ⚠️ → ✅ | The adapter sent no parameters. `limit` defaults to 10 (maximum 500), and the documented answer is a bare array with no way to tell it was cut short. The adapter now asks for pages the way Clerk's own SDKs do; the `{ data, total_count }` envelope is not in the spec but is what both SDKs decode. |
| `POST /redirect_urls` | `{ url }`; reads `id`, `url` | ✅ | Answer is the spec's `RedirectURL` (`object`, `id`, `url`, `created_at`, `updated_at`). |
| same, refusals | duplicate → re-read and claim | ❓ → hardened | The spec documents `400` and `422` (`ClerkErrors`) without saying which a duplicate gets or how it is worded. The adapter now checks the list after any `400`/`422`, not only one whose body says "exists". |
| `DELETE /redirect_urls/{id}` | `404` is success | ✅ | `200` answers a `DeletedObject`; `404` is documented. |

### Sim assumptions (`packages/sim/src/routes/clerk.ts`)

| Id | Assumption | Status |
| - | - | - |
| C1 | a duplicate answers `422` | ❓ (the adapter no longer depends on it) |
| C2 | bare array; with `paginated=true`, `{ data, total_count }` by `offset`/`limit` | array and parameters verified; envelope from the SDK sources (pinned by the contract suite) |
| C3 | `RedirectURL` and `DeletedObject` shapes | verified |

## LaunchDarkly

### Calls the adapter makes

| Call | What the adapter sends and reads | Spec | Notes |
| - | - | - | - |
| `GET /api/v2/flags/{projectKey}/{featureFlagKey}` | `env=<environment key>`; reads `variations[]._id`, `value`, `name`, and `environments.<key>.targets[]` / `contextTargets[]` (`contextKind`, `values`, `variation` index) | ✅ | `env` restricts the answer to one environment, as the reference recommends. `targets` holds kind `user`, `contextTargets` the other kinds; the adapter reads both and defaults a missing `contextKind` to `user`. `404` (unknown project or flag) fails the line; destroy treats it as already gone. |
| same, unknown environment | an environment the answer lacks is `PROVIDER_NOT_FOUND` naming it | ❓ | Whether LaunchDarkly answers `404` or omits the environment is not stated; the adapter reports either as not found. |
| `PATCH /api/v2/flags/{projectKey}/{featureFlagKey}` | `Content-Type: application/json; domain-model=launchdarkly.semanticpatch`; body `{ environmentKey, comment, instructions }` with `addTargets` and/or `removeTargets` `{ contextKind, values: [key], variationId }` | ✅ | Without the `domain-model` parameter the body is read as a JSON patch and refused with `400`. Moving a target sends `removeTargets` (old variation) and `addTargets` (new) in one patch: instructions are all or nothing, and `addTargets` is an error when the key would be targeted by two variations. The shared client gained `WriteOptions.contentType` for this. |
| same, answer | `200` with the whole flag; the adapter re-reads the target from it | ✅ | A target not served the wanted variation afterwards is `PROVIDER_RESPONSE`. |
| same, refusals | `405` (environment requires approvals) → `PROVIDER_INVALID` saying so; `409` (concurrent change) → re-read and retry, up to 3 times; `429` retried by the client | ✅ | Sponson does not open approval requests; use a preview or test environment without required approvals. `400` for a conflict with a pending scheduled change or approval is reported, not bypassed with `ignoreConflicts`. |

The PATCH is sent as idempotent (a 502/503/504 or dropped connection is retried): adding and removing an individual
target are set operations, so repeating one changes nothing more.

### Sim assumptions (`packages/sim/src/routes/launchdarkly.ts`)

| Id | Assumption | Status |
| - | - | - |
| LD1 | the access token is the whole `Authorization` value; base URL `https://app.launchdarkly.com/api/v2` | verified (the base URL from the OpenAPI document's location) |
| LD2 | `GET /flags/{p}/{f}?env=` answers `variations` and `environments` restricted to that environment; unknown project or flag `404` | verified, except how an unknown environment is answered ❓ |
| LD3 | `targets` (kind `user`) and `contextTargets` (other kinds), items `{ contextKind, values, variation }`; `contextTargets` also carries empty `user` placeholders | lists and items verified; the placeholders ❓ (the adapter ignores empty entries either way) |
| LD4 | semantic patch media type; `{ environmentKey, instructions, comment }`; `200` with the flag; all or nothing | verified |
| LD5 | `addTargets` refused when the key would be in two variations; re-adding a key to the variation that already serves it succeeds and changes nothing; `removeTargets` of an absent key does nothing | refusal verified (its status ❓); re-adding ❓ (pinned by the contract suite); removal verified |
| LD6 | `405` when approvals are required; `409` for a concurrent change; error body `{ code, message }` | verified |
| LD7 | a patch is visible to the next GET | ❓ (pinned by the contract suite) |

## PlanetScale

Paths are relative to `https://api.planetscale.com/v1` (the spec's `servers`) and, below the first row, to
`/organizations/{organization}/databases/{database}`.

### Calls the adapter makes

| Call | What the adapter sends and reads | Spec | Notes |
| - | - | - | - |
| every request | `Authorization: <PLANETSCALE_SERVICE_TOKEN_ID>:<PLANETSCALE_SERVICE_TOKEN>` | ✅ | Service tokens page; the spec's `securitySchemes` lists only OAuth. Variable names as in the CLI docs. Accesses needed: `create_branch`, `read_branch`, `delete_branch`, `connect_branch`, `delete_branch_password` (each endpoint's "Service Token Accesses"). |
| `GET /branches` (`list_branches`) | `page` from `next_page`; reads `data[].id`, `name`, `parent_branch`, `production`, `ready` | ✅ | Default `per_page` 25. Used by `listScope` only. |
| `GET /branches/{branch}` (`get_branch`) | by **name**; `404` → absent; reads `id`, `name`, `parent_branch`, `production`, `ready` | ✅ | `state` (`pending` … `ready`) is also returned; the adapter relies on the boolean `ready`. |
| `POST /branches` (`create_branch`) | `{ name, parent_branch }`; `201` | ✅ | `parent_branch` is optional in the spec (defaults to the database's default branch); the adapter always sends it, after checking it exists with `get_branch`. |
| same, duplicate name | any `409` or `422` → `get_branch` by name, claim it if found | ❓ | The spec lists `422` and no `409`; the wording is undocumented (PS5). |
| `DELETE /branches/{branch}` (`delete_branch`) | by name, after `get_branch` confirmed the id the ledger recorded; `404` is success | ✅ path and `204`; ❓ timing | Whether deletion is synchronous needs a live run (PS7). |
| `GET /branches/{branch}/passwords` (`list_passwords`) | `page` from `next_page`; reads `data[].id`, `name`, `role`, `username`, `access_host_url` | ✅ | `plain_text` is "Null except in the response from the create endpoint". The `q` search is not used (its matching rules are undocumented); the adapter filters names itself. |
| `POST /branches/{branch}/passwords` (`create_password`) | `{ name, role }`; reads `id`, `role`, `username`, `access_host_url`, `plain_text` | ✅ | Roles `reader`, `writer`, `admin`, `readwriter`. `name` is "optional"; uniqueness is not documented (PS9). |
| `DELETE /branches/{branch}/passwords/{id}` (`delete_password`) | `404` is success | ✅ | `204`. |
| connection string | `mysql://<username>:<plain_text>@<access_host_url>/<database>?ssl={"rejectUnauthorized":true}` | ✅ | The Node.js tutorial's form; Prisma's `sslaccept=strict` via `connection_params`. |

Not used: `PATCH …/passwords/{id}` takes only `name` and `cidrs` (✅), so a role cannot change in place (PS10), and
the adapter never calls `…/passwords/{id}/renew`, which would rotate the plaintext.

### Sim assumptions (`packages/sim/src/routes/planetscale.ts`)

| Id | Assumption | Status |
| - | - | - |
| PS1 | `Authorization: <id>:<token>`; the sim refuses `Bearer` | verified (pinned by the contract suite) |
| PS2 | paths under organization and database; branches addressed by name | verified |
| PS3 | `{ data, next_page }`, `?page=`, 25 per page | verified |
| PS4 | a created branch is `ready: false` at first and becomes ready later | fields verified; initial state through the CLI's `--wait` only; duration ❓ (pinned) |
| PS5 | a duplicate branch name answers `422` | ❓ (the adapter accepts `409` or `422`) |
| PS6 | an unknown parent answers `404` | ❓ (the adapter checks the parent first) |
| PS7 | branch deletion is synchronous and takes the passwords with it | `204` verified; timing ❓ |
| PS8 | `plain_text` only in the create answer | verified (pinned) |
| PS9 | password names are not unique | ❓ (the adapter refuses two of one name) |
| PS10 | a password's role cannot change in place | verified |
| PS11 | password delete `204`; `404` when gone | `204` verified; `404` ❓ |
| PS12 | a password on a branch that is not ready is refused | ❓ (the adapter waits for `ready` first, so either answer is fine) |
| PS13 | error bodies are `{ code, message }` | ❓ (never read by the adapter) |
| PS14 | branch name characters | ❓ (the default name uses only `a-z`, `0-9`, `-`) |

## The generic `http` adapter

The `http` adapter makes the calls a plan line declares, so there is no provider to verify it against: a line is
checked by running `sponson plan` (it only reads) against the real API before the first `apply`. The plans in
[examples/](/reference/examples) that use it are illustrative and say so.

[Recipes](/reference/recipes) are the exception: each is verified against the provider pages it cites, its assumptions are
numbered and marked verified or not in its file (and listed in docs/recipes.md), and `pnpm test:live` runs it against
the real API when its credential and `SPONSON_LIVE_*` variables are set ([ADR 0020](/contributing/adr/0020-recipes)).

### Sim assumptions (`packages/sim/src/routes/rest.ts`)

They describe a conventional JSON REST API, not one provider; the adapter's tests and scenarios run against them.

| Id | Assumption | Status |
| - | - | - |
| R1 | credentials as `Authorization: Bearer`, `Authorization: Basic`, a bare token in `Authorization` (Unleash) or a header ending in `-Api-Key`, `-Key` or `-Token` | convention |
| R2 | `{ data }` envelopes for collection objects and lists, cursor in `next_cursor`; other objects bare; a collection's style changes the envelopes, cursor and id field; an object's style may give its reads an `item_path` envelope; a create may be posted to an alias of its collection | convention (the plan line names its own envelope with `item_path`, `list_path`, `find.next`; the recipe tests shape the style from the recipe) |
| R3 | unknown paths are 404 until seeded or created; client-chosen ids; duplicate `name` → 409; a style may name objects by a query parameter (`item_query`: `?url=`), and let a create choose the id by one (`id_query`: `?channelId=`) | convention (`exists_status` covers providers that answer otherwise; the queries follow Stytch's management API, SY4 in its recipe, and Firebase Hosting's `channels.create`, FH2 in its recipe) |
| R4 | PATCH (and POST to an object, as Stripe updates) is a JSON merge patch (RFC 7396); PUT replaces; JSON or form bodies, form values typed under the style's `form_types`; no preconditions | RFC 7396 for PATCH; the rest convention; Stripe's typed form parameters (ST1) |
| R5 | DELETE answers `{ id, deleted }`; unknown is 404 | convention (`gone_status` covers providers that answer otherwise) |
| R6 | a style's `once_fields` appear only in the create's answer, never stored or listed | Stripe's webhook signing secret (ST5–ST6 in its recipe) |
| R7 | with an `auth` seed, credentials are checked for real: an OAuth2 client-credentials token endpoint, fixed bearer tokens, HTTP Digest, SigV4 | RFC 6749 §4.4, RFC 7616 (its §3.9.1 example is a test vector), AWS SigV4 (its published test suite is a test vector; ../auth.ts is written independently of the client) |
| R8 | a path the seed lists in `reads` is a read sent as a POST: it answers the object it names and is not a write | Azure Resource Manager list actions (`POST …/builds/{environmentName}/listAppSettings`, [https://learn.microsoft.com/en-us/rest/api/appservice/static-sites/list-static-site-build-app-settings](https://learn.microsoft.com/en-us/rest/api/appservice/static-sites/list-static-site-build-app-settings)) |
| R10 | a style's `generated` fields are generated on create and stored; with the auth seed's `generated_header`, that header is accepted when it carries a value the sim generated | Postmark's server token (`ApiTokens`, PM4 in its recipe), which authenticates the Server API (PS1) |
| R11 | a seeded `deletes` path takes a POST naming objects of a collection in its body (or, with a `{id}` segment, in its path) and removes them; 404 when none exists | Datadog's `POST /synthetics/tests/delete` (DD5 in its recipe); Convex's `POST /deployments/{name}/delete` (CX5 in its recipe) |

### Authentication forms (`packages/adapters/src/http-adapter-auth.ts`)

Checked on 2026-10-11 against the providers' docs and official clients; the sims and scenarios encode them.

| Provider | Form | Source | Status |
| - | - | - | - |
| Auth0 Management API | `POST https://{domain}/oauth/token`, form `grant_type=client_credentials`, `client_id`, `client_secret`, `audience=https://{domain}/api/v2/`; answer `{ access_token, expires_in, token_type: Bearer }` | [https://auth0.com/docs/secure/tokens/access-tokens/management-api-access-tokens/get-management-api-access-tokens-for-production](https://auth0.com/docs/secure/tokens/access-tokens/management-api-access-tokens/get-management-api-access-tokens-for-production) | ✅ verified |
| Kinde Management API | `POST https://{sub}.kinde.com/oauth2/token`, form with `audience=https://{sub}.kinde.com/api` | [https://docs.kinde.com/developer-tools/kinde-api/access-token-for-api/](https://docs.kinde.com/developer-tools/kinde-api/access-token-for-api/) | ✅ verified |
| Okta | Custom authorization server `/oauth2/default/v1/token` with `client_secret_basic`: ✅. The org server's `okta.*` (management API) scopes accept only `private_key_jwt`, which is not supported | [https://developer.okta.com/docs/guides/implement-grant-type/clientcreds/main/](https://developer.okta.com/docs/guides/implement-grant-type/clientcreds/main/), [https://developer.okta.com/docs/guides/implement-oauth-for-okta-serviceapp/main/](https://developer.okta.com/docs/guides/implement-oauth-for-okta-serviceapp/main/) | ✅ verified; management API ⚠️ not covered |
| MongoDB Atlas | Admin API v2 `https://cloud.mongodb.com/api/atlas/v2`, `Accept: application/vnd.atlas.2023-01-01+json` (406 without); API keys by HTTP Digest (OpenAPI `DigestAuth`); service accounts by client credentials at `https://cloud.mongodb.com/api/oauth/token` (Basic) | [https://github.com/mongodb/openapi](https://github.com/mongodb/openapi) (`openapi/v2.json`), [https://www.mongodb.com/docs/atlas/api/api-authentication/](https://www.mongodb.com/docs/atlas/api/api-authentication/) | ✅ verified; the challenge's `algorithm=MD5, qop="auth"` ❓ (seen in a third-party report only; both algorithms are supported) |
| Google Cloud | `gcloud auth print-access-token` as a bearer token; Identity Toolkit `PATCH admin/v2/projects/{p}/config?updateMask=authorizedDomains`; user credentials also need `x-goog-user-project` | [https://cloud.google.com/identity-platform/docs/reference/rest/v2/projects/updateConfig](https://cloud.google.com/identity-platform/docs/reference/rest/v2/projects/updateConfig), [https://cloud.google.com/docs/authentication/troubleshoot-adc](https://cloud.google.com/docs/authentication/troubleshoot-adc) | ✅ verified (quota-project header from the troubleshooting page) |
| Azure | `az account get-access-token --query accessToken -o tsv` | [https://learn.microsoft.com/en-us/cli/azure/account#az-account-get-access-token](https://learn.microsoft.com/en-us/cli/azure/account#az-account-get-access-token) | ✅ verified |
| AWS | SigV4 header signing; S3 requires `x-amz-content-sha256` | [https://docs.aws.amazon.com/IAM/latest/UserGuide/reference\_sigv-create-signed-request.html](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-create-signed-request.html); test vectors from [https://github.com/awslabs/aws-c-auth/tree/main/tests/aws-signing-test-suite/v4](https://github.com/awslabs/aws-c-auth/tree/main/tests/aws-signing-test-suite/v4) (`get-vanilla`, `get-vanilla-query-order-key-case`, `post-vanilla`, `post-x-www-form-urlencoded`) | ✅ verified |

### Sim assumptions (`packages/sim/src/routes/aws.ts`)

| Id | Assumption | Status |
| - | - | - |
| A1 | requests signed with SigV4 for the path's service and region; S3 requests carry `x-amz-content-sha256` | ✅ verified (IAM signing guide; S3 common request headers) |
| A2 | `PUT /?cors` takes 1–100 `CORSRule`s, each with an `AllowedOrigin` and an `AllowedMethod`, requires `Content-MD5` (or an `x-amz-checksum-*`), replaces the whole configuration, answers 200 with no body; empty is `MalformedXML` | ✅ verified ([https://docs.aws.amazon.com/AmazonS3/latest/API/API\_PutBucketCors.html](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketCors.html); botocore `requestChecksumRequired`) |
| A3 | `GET /?cors` answers the configuration as XML, 404 `NoSuchCORSConfiguration` without one; `DELETE /?cors` answers 204 | ✅ verified for GET and DELETE (API\_GetBucketCors, API\_DeleteBucketCors); the 404 code ❓ |
| A4 | Amplify `GetBranch` / `UpdateBranch` (`GET`/`POST /apps/{appId}/branches/{branchName}`) answer `{ branch }`; `environmentVariables` replaces the whole map; errors are JSON with `x-amzn-ErrorType` | ✅ verified ([https://docs.aws.amazon.com/amplify/latest/APIReference/API\_UpdateBranch.html](https://docs.aws.amazon.com/amplify/latest/APIReference/API_UpdateBranch.html), API\_GetBranch.html; signing name `amplify` from botocore) |

### Sim assumptions (`packages/sim/src/routes/railway.ts`, `packages/sim/src/routes/fly.ts`)

| Id | Assumption | Status |
| - | - | - |
| RW1 | one endpoint `https://backboard.railway.com/graphql/v2`; account or workspace tokens as `Authorization: Bearer`, project tokens as `Project-Access-Token` | ✅ verified ([https://docs.railway.com/reference/public-api](https://docs.railway.com/reference/public-api)) |
| RW2 | failures are HTTP 200 with `errors[]`; a refused token is `INTERNAL_SERVER_ERROR` "Not Authorized"; parse and validation errors are 400 | ✅ verified (same page) |
| RW3 | `variables(projectId, environmentId, serviceId, unrendered)` answers the `EnvironmentVariables` scalar, a flat name → value map | ✅ verified ([https://github.com/railwayapp/cli](https://github.com/railwayapp/cli), `src/gql/schema.json`) |
| RW4 | `variableUpsert(input: VariableUpsertInput!): Boolean!` and `variableDelete(input: VariableDeleteInput!): Boolean!` with `projectId`, `environmentId`, `serviceId`, `name` (and `value`) | ✅ verified (same schema); deleting an absent variable answers `true` ❓ |
| RW5 | an unknown environment is an error "Environment not found" | ❓ wording |
| F1 | `Authorization: Bearer <token>`, or `FlyV1 <macaroon>` for tokens from `fly tokens create` | ✅ verified ([https://github.com/superfly/fly-go](https://github.com/superfly/fly-go), `tokens/tokens.go`) |
| F2 | `app(name) { secrets { name digest createdAt } }`; values never returned | ✅ verified (fly-go `resource_secrets.go`, `schema.graphql`) |
| F3 | `setSecrets(input: { appId, secrets: [{ key, value }], replaceAll })` and `unsetSecrets(input: { appId, keys })` answer `{ app, release }` | ✅ verified (fly-go `schema.graphql`); unsetting an absent key ❓ |
| F4 | failures are HTTP 200 with `errors[]`; an unknown app is "Could not find App" | ❓ wording. Current flyctl sets secrets through the Machines API (`api.machines.dev/v1/apps/{app}/secrets`, documented only in fly-go's source); the GraphQL mutations are not marked deprecated |

## Supabase

Written against the spec from the start (the adapter is newer than this page), so there is no ⚠️ column history: every
call below was checked before the first commit. Base URL `https://api.supabase.com/v1`; the spec's paths carry the
`/v1` prefix.

### Calls the adapter makes

| Call | What the adapter sends and reads | Spec | Notes |
| - | - | - | - |
| `GET /v1/projects/{ref}/branches` | reads `[].name`, `project_ref`, `is_default` | ✅ | `v1-list-all-branches`: a bare array of `BranchResponse`, no page parameters. The default branch is skipped and never managed. |
| `POST /v1/projects/{ref}/branches` | `{ branch_name }`; reads `name`, `project_ref` from the `201` | ✅ | `CreateBranchBody` requires only `branch_name`; `git_branch`, `persistent`, `with_data`, `region` and the rest are optional and not sent. |
| same, refusals | a duplicate name → re-read and claim; any other refusal reported | ❓ | Only `201`, `401`, `403`, `429` and `500` are documented. The adapter checks the list after a `409`, `400` or `422` before reporting (S5). A `500` ("Failed to create database branch") is not retried: the create may have happened, and the intent plus find-by-name recovers it on the next run. |
| `GET /v1/branches/{branch_id_or_ref}` | the branch's ref; reads `status`, `db_host`, `db_port`, `db_user`, `db_pass` | ✅ fields; ❓ readiness | `BranchDetailResponse`. `status` is the project status enum; `ACTIVE_HEALTHY` is taken as ready and `INIT_FAILED`, `REMOVED`, `GOING_DOWN`, `INACTIVE`, `PAUSING`, `PAUSE_FAILED`, `RESTORE_FAILED` as never-ready. `db_user` and `db_pass` are optional in the spec; without them the line fails with `PROVIDER_RESPONSE` rather than outputting a broken URI. A `404` while the branch comes up is treated as "not yet". The ref form of the path parameter is used: the uuid form is marked deprecated. |
| `DELETE /v1/branches/{branch_id_or_ref}` | the branch's ref; `404` is success | ✅ path, `200`; ❓ `404` | "By default, deletes immediately" (`force=false` would schedule it). No `404` is documented; an unknown ref's status needs a live run. The parent project's own ref is never sent. |
| `GET /v1/projects/{ref}/config/auth` | reads `uri_allow_list` (nullable string) | ✅ | `AuthConfigResponse`; `uri_allow_list` is required and nullable. |
| `PATCH /v1/projects/{ref}/config/auth` | `{ uri_allow_list }` alone; retried as idempotent | ✅ | `UpdateAuthConfigBody` has no required field, so a body with one field touches one field. Sending only that field also avoids the reported failure when re-sending a whole config with auth hooks enabled (supabase/supabase#36861). No ETag or version: the write is unconditional, so Sponson's writers hold a lock on the list (`lockOn`, ADR 0019), and the adapter re-reads and repeats (three rounds) when a writer outside Sponson overwrote it. |

Derived outputs: `api_url` is `https://<ref>.supabase.co` (the project URL format in the API docs) and
`connection_string` is `postgresql://<db_user>:<db_pass>@<db_host>:<db_port>/postgres`, matching the documented
direct connection string (`postgres` database; the password percent-encoded). The direct host resolves over IPv6
unless the project has the IPv4 add-on.

### Sim assumptions (`packages/sim/src/routes/supabase.ts`)

| Id | Assumption | Status |
| - | - | - |
| S1 | create takes `{ branch_name }` and answers `201` `BranchResponse` with the branch's own `project_ref` | verified (schema); that `project_ref` is usable before provisioning finishes ❓ (pinned) |
| S2 | the list is a bare unpaged array, and includes the project's own default branch | array verified; the default branch's presence ❓ (pinned; the adapter skips it either way) |
| S3 | the detail answers `200` with `COMING_UP` until ready, then `ACTIVE_HEALTHY` | fields and enum verified; the progression and the `200` while coming up ❓ (pinned) |
| S4 | detail carries `db_user`/`db_pass`; database `postgres`; API URL `https://<ref>.supabase.co` | optional fields verified; that a personal access token gets `db_pass` ❓ (pinned); database name and URL verified in the docs |
| S5 | a duplicate branch name answers `409` | ❓ (pinned; the adapter accepts `400`/`409`/`422`) |
| S6 | delete answers `200 { message: "ok" }`, gone from the list at once; unknown ref `404` | `200` and immediacy of the default verified; list visibility and `404` ❓ (pinned) |
| S7 | `uri_allow_list` nullable string; a one-field PATCH changes one field; no precondition | verified |
| S8 | the allow-list is comma-separated; stored verbatim | format verified (Auth's `URI_ALLOW_LIST`); verbatim storage ❓ (pinned) |
| S9 | a branch has its own Auth config at `/v1/projects/{branch ref}/config/auth` | ❓ |

## Netlify

Written against the specification and docs from the start (no earlier version to correct), so the table records
what each call relies on rather than fixes.

### Calls the adapter makes

| Call | What the adapter sends and reads | Spec | Notes |
| - | - | - | - |
| `GET /sites/{site_id}` | reads `name`, `account_id` (else `account_slug`) | ✅ | getSite. Only to name the account when the plan does not, and to build URLs from `name`. That `name` is the `<name>.netlify.app` subdomain is the docs' example (`mysitename.netlify.app`), not a statement (NL2, pinned). |
| `GET /accounts/{account_id}/env` | `site_id`; reads `[].key`, `values[].id`, `value`, `context`, `context_parameter`, `is_secret`, `updated_at` | ✅ | getEnvVars; "If specified, only return environment variables set on this site", so shared (account) variables never enter a line's scope. `{account_id}` may be the slug (API guide). No page parameters are documented; the adapter reads one answer. |
| `POST /accounts/{account_id}/env` | `site_id`; body `[{ key, values: [{ context, value, context_parameter? }] }]`; reads `201` list of `envVar` | ✅ | createEnvVars. `scopes` and `is_secret` are left out: "By default, environment variables apply to all scopes" (overview); a variable Sponson creates is not secret, so its values stay comparable. |
| same, an existing key | re-list, then PATCH instead | ❓ | The status is not documented (NL5); the adapter treats `400`, `409` and `422` alike and only falls back when the re-list shows every key. Between Sponson runs this cannot happen: lines lock `netlify:<site>:env` (ADR 0019); it covers a person or another tool creating the variable meanwhile. |
| `PATCH /accounts/{account_id}/env/{key}` | `site_id`; body `{ context, value, context_parameter? }`; reads `201` `envVar` | ✅ | setEnvVarValue: "updates or creates a new value for an existing environment variable". Sent as idempotent (it sets state). That it leaves other contexts' values and keeps the replaced value's id is ❓ (NL3, pinned). |
| `DELETE /accounts/{account_id}/env/{key}/value/{id}` | `site_id`; `404` is success | ✅ | deleteEnvVarValue, `204`. Destroy removes only the line's values. |
| `GET /accounts/{account_id}/env/{key}` | `site_id`; reads `values` | ✅ | getEnvVar, after a value delete: is anything left? |
| `DELETE /accounts/{account_id}/env/{key}` | `site_id`; `404` is success | ✅ | deleteEnvVar, `204`; only when the variable has no value left (NL6). Netlify offers no precondition, so the check and the delete run under the `netlify:<site>:env` lock: another Sponson scope cannot add a value in between. |
| `GET /sites/{site_id}/deploys` | `page=1&per_page=100`; reads `[].id`, `state`, `context`, `commit_ref`, `review_id`, `created_at` | ✅ fields; ❓ order and `context` values | listSiteDeploys; `page`/`per_page` from the API guide. `state` values from the operation's `state` filter enum (`ready`; `error`, `rejected` treated as failed; the rest in progress). `context` and `review_id` have no enum or description in the spec (NL7, pinned). The adapter sorts by `created_at` and filters by commit itself. |
| `POST /sites/{site_id}/builds` | `branch` (absent: production) | ✅ | createSiteBuild: "No branch means production … Otherwise it's a branch deploy." Used to rebuild a production or branch deploy that predates a write. Whether it builds a branch whose branch deploys are off is ❓ (NL11). |

Deploy Previews cannot be rebuilt through any documented call (the docs describe "Retry deploy" in the UI only), so a
`context: deploy-preview` line reports the stale deploy (`notes.stale_deploy`) and waits for a newer one.

### Sim assumptions (`packages/sim/src/routes/netlify.ts`)

| Id | Assumption | Status |
| - | - | - |
| NL1 | base URL `https://api.netlify.com/api/v1`, bearer | ✅ verified |
| NL2 | env paths, statuses (`201`, `204`); account slug accepted; site `name`, `account_id`, `account_slug` | ✅ verified; `name` = subdomain ❓ (pinned) |
| NL3 | `envVar` / `envVarValue` shapes and contexts; `branch` needs `context_parameter`; default scopes | ✅ verified; PATCH leaves other values and keeps the id ❓ (pinned) |
| NL4 | secret values write-only except `dev` | ✅ verified (Secrets Controller); how a masked value is returned ❓ (the adapter ignores it) |
| NL5 | duplicate key on POST → `400`; `all` cannot sit with contextual values | ❓ (pinned: `400` or `409`) |
| NL6 | a variable keeps existing with no values | ❓ (pinned: the adapter deletes it either way) |
| NL7 | deploy list newest first, its fields; `context` values; `review_id` = PR number | fields ✅; order and values ❓ (pinned) |
| NL8 | deploy `state` values | ✅ verified (enum of the `state` filter) |
| NL9 | permalink and Deploy Preview URL formats | ✅ verified (deploy overview) |
| NL10 | builds read the values set when they started; `updated_at` changes with a value | first ✅ verified; that PATCH bumps `updated_at` ❓ (pinned) |
| NL11 | `POST /sites/{site_id}/builds` with and without `branch` | ✅ verified; branch deploys turned off ❓ |
| NL12 | a `branch` value reaches that branch's Deploy Previews | ✅ verified ("used for deploy permalinks, Deploy Previews, and branch deploys for the specified branch") |

## Cloudflare

### Calls the adapter makes

| Call | What the adapter sends and reads | Spec | Notes |
| - | - | - | - |
| base URL and auth | `https://api.cloudflare.com/client/v4`, `Authorization: Bearer <CLOUDFLARE_API_TOKEN>` | ✅ | [Make API calls](https://developers.cloudflare.com/fundamentals/api/how-to/make-api-calls/). The token needs the account permission "Cloudflare Pages Edit" ([permissions](https://developers.cloudflare.com/fundamentals/api/reference/permissions/); schema token groups `Pages Read`/`Pages Write`). |
| `GET /accounts/{account_id}/pages/projects/{project_name}` | reads `success`, `result.deployment_configs.{preview,production}.env_vars`: NAME → `{ type, value }` | ✅ | Schema `pages_project` (required `deployment_configs`, each with `env_vars`, a nullable map of `pages_plain_text_env_var` / `pages_secret_text_env_var`). A null `env_vars` or entry is read as empty / absent. |
| same, secrets | a `secret_text` value is never used, whatever the answer holds | ❓ → hardened | The docs say a secret cannot be seen after it is set ([bindings](https://developers.cloudflare.com/pages/functions/bindings/)); the schema's example is `{"type":"secret_text","value":""}` (marked `x-sensitive`). Not stated whether `value` is `""`, null or absent; the adapter ignores it in every case (CF3). |
| `PATCH` of the same path | body `{ deployment_configs: { <target>: { env_vars: { NAME: { type, value } \| null } } } }`, sent as idempotent | ✅ `null`, ❓ merge | "To delete an environment variable, set its key to `null`" (schema and edit reference). That keys not in the body are kept is implied (the rule, and no required body field) but not stated; the adapter re-reads after every write and fails with `PROVIDER_RESPONSE` naming any key it did not send that disappeared (CF4, pinned). Answer `200` with the project: ✅. |
| same, concurrency | none | ✅ (none exists) | No `If-Match`/ETag parameter and no `412` on this operation (CF5). Sponson's own writers of one map are serialised by the parent lock `cloudflare:<account>:pages:<project>:<target>:env` (ADR 0019); writers outside Sponson race, last write wins. |
| failures | `4XX { success: false, errors: [{ code, message }] }`; unknown project → `404` | ✅ shape, ❓ `404` | Schema `pages_api-response-common-failure`; it declares only `4XX`. `404` with code `8000007` comes from Wrangler's error reports, not the spec (CF6). The shared client classifies by status. |
| rate limit | `429` retried, honouring `Retry-After` | ✅ | "1,200 requests per five minute period per user"; `Retry-After` in seconds ([limits](https://developers.cloudflare.com/fundamentals/api/reference/limits/)). |

### Sim assumptions (`packages/sim/src/routes/cloudflare.ts`)

| Id | Assumption | Status |
| - | - | - |
| CF1 | base URL, bearer token, "Pages Edit" permission | ✅ verified |
| CF2 | GET envelope and `deployment_configs.<env>.env_vars` shape | ✅ verified |
| CF3 | a secret's value is never returned; the entry keeps its type | partly verified (docs and schema example); exact form ❓ (pinned by the contract suite) |
| CF4 | PATCH merges the map; `null` deletes; `200` with the project | `null` and `200` ✅; merge ❓ (pinned) |
| CF5 | no precondition on the PATCH | ✅ verified |
| CF6 | failure envelope; unknown project `404` / `8000007` | envelope ✅; status and code ❓ |
| CF7 | a write is visible to the next GET | ❓ (pinned) |
| CF8 | one set of preview variables for every preview deployment | ✅ verified: "This will set the configuration for all preview deployments, not just the deployments from a specific branch" ([Wrangler configuration](https://developers.cloudflare.com/pages/functions/wrangler-configuration/)) |
| CF9 | no documented pattern for variable names | ✅ (schema map keys are free-form) |
| CF10 | a variable reaches only later deployments | indirectly documented ("Redeploy your project for the binding to take effect", [bindings](https://developers.cloudflare.com/pages/functions/bindings/)); the adapter does not redeploy |

### Not built, and why

* **Preview URL output.** [Preview deployments](https://developers.cloudflare.com/pages/configuration/preview-deployments/)
  documents the branch alias `<alias>.<project>.pages.dev` and that "Branch name aliases are lowercased and
  non-alphanumeric characters are replaced with a hyphen", but not the shortening of long names (a 28-character
  limit appears only in community answers) or what happens on collisions, and the host is the project's `subdomain`,
  which may differ from its name. Computing it from the branch would sometimes be wrong, so `pages_env` has no
  outputs. The deployment object's documented `aliases` list is the reliable source, for a later deployment op.

## Algolia

### Calls the adapter makes

| Call | What the adapter sends and reads | Spec | Notes |
| - | - | - | - |
| base URL and auth | `https://<application_id>.algolia.net`, headers `X-Algolia-Application-Id` and `X-Algolia-API-Key: <ALGOLIA_API_KEY>` | ✅ | `spec.yml` servers and security schemes; the CLI reads `ALGOLIA_API_KEY` (AL2). |
| `GET /1/indexes/{name}/settings` | existence only; `404` means absent | ✅ | `settings.yml` documents `404` "Index not found" (AL4). Used for the line's index and to refuse a template that does not exist. |
| `POST /1/indexes/{copy_from}/operation` | `{ operation: "copy", destination, scope? }`; reads `taskID` | ✅ | `operationIndex.yml` (AL3). Overwrites an existing destination; copying a missing source makes an empty index, hence the adapter's check. Sent as idempotent: a repeated copy writes the same template again. |
| `GET /1/indexes/{name}/task/{taskID}` | `status` until `published` | ✅ | `getTask.yml` (AL5). Whether the destination is visible before then is not stated ❓; the sim shows it only when published. |
| `DELETE /1/indexes/{name}` | answer not read | ✅ | Deleting a missing index "is ignored without warning" (AL6); a `404` is treated as gone too. |

### Sim assumptions (`packages/sim/src/routes/algolia.ts`)

| Id | Assumption | Status |
| - | - | - |
| AL1 | the index name is its identity in the application | ✅ verified |
| AL2 | host and the two authentication headers; `ALGOLIA_API_KEY` | ✅ verified |
| AL3 | copy answers `{ taskID }`; scope; overwrite; a missing source makes an empty index | ✅ verified |
| AL4 | settings of a missing index answer `404` | ✅ verified |
| AL5 | task status `published` / `notPublished`; destination absent until published | status ✅; visibility ❓ (pinned) |
| AL6 | deleting a missing index is ignored | ✅ verified (status ❓) |
| AL7 | the index list without `page` is unpaginated | ✅ verified (not used by the adapter) |
| AL8 | a copy is published within minutes | ❓ (the wait is bounded by `SPONSON_ALGOLIA_TASK_TIMEOUT_MS`) |

## Xata

### Calls the adapter makes

| Call | What the adapter sends and reads | Spec | Notes |
| - | - | - | - |
| base URL and auth | `https://api.xata.tech`, `Authorization: Bearer <XATA_API_KEY>` | ✅ | Spec security scheme `apiKey`; the CLI reads `XATA_API_KEY` (XA1). |
| `GET /organizations/{org}/projects/{project}/branches` | `branches[].id`, `.name` | ✅ | Unpaginated (XA3). Name uniqueness is not documented ❓: more than one match is refused. |
| `POST` of the same path | `{ mode: "inherit", parentID, name }`; reads `id`, `name` | ✅ | `201`, no `409` documented (XA2), hence the lookup by name before every create. |
| `GET …/branches/{branchID}` | `status.statusType` until it ends in `HEALTHY`; one ending in `FAULT` fails | ✅ field, ❓ spelling | The CLI's `branch wait-ready` "blocks until the branch is healthy" (XA5). |
| `GET …/branches/{branchID}/credentials?username=xata` | `connectionString` | ✅ | (XA6). The branch's own `connectionString` field is deprecated; not used. |
| `DELETE …/branches/{branchID}` | answer not read; `404` is gone | ✅ | Permanent (XA4). |

### Sim assumptions (`packages/sim/src/routes/xata.ts`)

| Id | Assumption | Status |
| - | - | - |
| XA1 | base URL, bearer API key, `XATA_API_KEY` | ✅ verified |
| XA2 | create a child by `parentID` with `mode: inherit`; `201` with the branch | ✅ verified |
| XA3 | the list is `{ branches }`, unpaginated; names may repeat | list ✅; uniqueness ❓ |
| XA4 | detail with `status`; `404`; permanent `DELETE` (`204`) | ✅ verified |
| XA5 | ready when healthy, broken on a fault | field ✅; enum spelling ❓ (pinned) |
| XA6 | credentials readable at any time | endpoint ✅; "any time" inferred |
| XA7 | ready within minutes | ❓ (bounded by `SPONSON_XATA_READY_TIMEOUT_MS`) |

## Contentful

### Calls the adapter makes

| Call | What the adapter sends and reads | Spec | Notes |
| - | - | - | - |
| base URL and auth | `https://api.contentful.com`, `Authorization: Bearer <CONTENTFUL_MANAGEMENT_TOKEN>`, `Content-Type: application/vnd.contentful.management.v1+json` | ✅ | The variable is the one Contentful's Terraform provider and migration tool read (CF1, CF3). |
| `GET /spaces/{space}/environments/{id}` | `sys.id`, `sys.status.sys.id`; `404` is absent | ✅ | Polled until `ready` (`failed` fails), as the CLI's `--await-processing` does (CF4, CF5). |
| `PUT` of the same path | `{ name }`, `X-Contentful-Source-Environment: <source>`; `201` with status `queued` | ✅ | A `409` (it exists: a create whose answer was lost) is read and waited for, claimed by the engine (CF2, CF6 ❓). |
| `DELETE` of the same path | `204`; `404` is gone | ✅ | Never `master` (CF5). |
| `GET /spaces/{space}/api_keys/{id}` | `name`, `description`, `environments[].sys.id`, `sys.version` | ✅ | (CF7) |
| `PUT` of the same path | `{ name, description, environments }` with `X-Contentful-Version`; reads the answer's `environments` | ✅ | The fields the official SDK sends; a `409` is read and written again, three rounds at most (CF6, CF7). |

### Sim assumptions (`packages/sim/src/routes/contentful.ts`)

| Id | Assumption | Status |
| - | - | - |
| CF1 | base URL, bearer token, `CONTENTFUL_MANAGEMENT_TOKEN` | ✅ verified (EU host ❓) |
| CF2 | `PUT` with an id creates, copied from the source header (default `master`); `201`, `queued` | ✅ verified |
| CF3 | the CMA media type on every body | ✅ verified (whether plain JSON is refused ❓; the sim refuses it) |
| CF4 | `queued` → `inProgress` → `ready` / `failed`, read on the environment | ✅ verified (pinned) |
| CF5 | `404 NotFound`; `DELETE` `204`; `master` cannot be deleted | ✅ verified |
| CF6 | an update needs the current version, else `409 VersionMismatch` | rule ✅; the status for a create on an existing id ❓ |
| CF7 | an API key reads only the environments it links; `PUT` with the version replaces them | ✅ verified (pinned) |
| CF8 | the environment list envelope | ✅ verified (not used by the adapter) |
| CF9 | 151 environments per space, 12 creations per 5 minutes | ✅ verified (per-plan counts ❓) |
| CF10 | a copy is ready within minutes | ❓ (bounded by `SPONSON_CONTENTFUL_READY_TIMEOUT_MS`) |

## DigitalOcean

### Calls the adapter makes

| Call | What the adapter sends and reads | Spec | Notes |
| - | - | - | - |
| base URL and auth | `https://api.digitalocean.com`, `Authorization: Bearer <DIGITALOCEAN_ACCESS_TOKEN>` | ✅ | (DO1) |
| `GET /v2/apps?per_page=100&page=` | `apps[].id`, `.spec`, `.live_url`; follows `links.pages.next` | ✅ | No name filter; the adapter matches `spec.name` as DigitalOcean's own GitHub action does (DO2). |
| `POST /v2/apps` | `{ spec }`; reads `app.id`, `app.pending_deployment.id` | ✅ | Duplicate name: unique per account, status undocumented ❓; any conflict is re-read and claimed (DO3). |
| `PUT /v2/apps/{id}` | `{ spec }`, the whole spec | ✅ | Replaces the spec; starts a deployment (DO4, inferred). |
| `GET /v2/apps/{app_id}/deployments/{deployment_id}` | `deployment.phase` until `ACTIVE` or `SUPERSEDED`; `ERROR`/`CANCELED` fail | ✅ | (DO5) |
| `DELETE /v2/apps/{id}` | answer not read; `404` is gone | ✅ | (DO8) |

### Sim assumptions (`packages/sim/src/routes/digitalocean.ts`)

| Id | Assumption | Status |
| - | - | - |
| DO1 | base URL, bearer token, `DIGITALOCEAN_ACCESS_TOKEN` | ✅ verified |
| DO2 | create/get/list shapes; no name filter | ✅ verified |
| DO3 | names unique per account; duplicate status | uniqueness ✅; status ❓ |
| DO4 | `PUT` replaces the whole spec and deploys | ✅ replace; deploy inferred |
| DO5 | `pending_deployment`; deployment phases | ✅ verified |
| DO6 | stored spec adds defaults, keeps sent fields and list order | ❓ (pinned) |
| DO7 | `SECRET` values read back encrypted (`EV[…]`) | encryption ✅; read-back form ❓ |
| DO8 | `DELETE` removes the app | ✅ verified |

## Google Cloud Run

### Calls the adapter makes

| Call | What the adapter sends and reads | Spec | Notes |
| - | - | - | - |
| base URL and auth | `https://run.googleapis.com/v2/…`, `Authorization: Bearer <the token gcloud auth print-access-token prints>` | ✅ | `cloud-platform` scope on every method (CR1). |
| `GET /v2/projects/{p}/locations/{r}/services/{s}` | `etag`, `template`, `traffic`, `trafficStatuses[].tag`/`.uri`, `latestReadyRevision` | ✅ | Revision names taken by their last path segment: full or short is undocumented ❓ (CR2, CR7). |
| `PATCH …/services/{s}?updateMask=template[,traffic]` | `{ template, traffic?, etag }`: the service's template with `revision`, the container's `image` and `env` set; every LATEST target bound to `latestReadyRevision` (only with `pin_latest: true`; otherwise refused before this call) | ✅ | Answers an Operation (CR3); a template change makes a revision (CR4); the pin keeps traffic where it is, as `--no-traffic` does (CR5). Etag as precondition ❓ (CR8). |
| `PATCH …/services/{s}?updateMask=traffic` | `{ traffic, etag }`: the list as read, the tag moved onto the revision (tag, add) or removed (destroy); shares untouched | ✅ | (CR5, CR6) |
| `GET /v2/{operation name}` | `done`, `error.message`/`.code` | ✅ | Polled until `done` (CR9); when a template change's operation ends is ❓. |
| `GET …/services/{s}/revisions/{rev}` | `containers[].name`/`.image`/`.env[].value`, `conditions[type=Ready].state` | ✅ | (CR10) |

### Sim assumptions (`packages/sim/src/routes/cloud_run.ts`)

| Id | Assumption | Status |
| - | - | - |
| CR1 | base URL, bearer token from gcloud, `cloud-platform` scope | ✅ verified |
| CR2 | Service fields; `generation` as a JSON string | ✅ verified |
| CR3 | `PATCH` with `updateMask` answers an Operation, writes only masked fields | ✅ verified |
| CR4 | a template change makes a revision; `template.revision` names it; naming rules | ✅ verified; refusal of a taken name ❓ |
| CR5 | TrafficTarget shape; LATEST follows the latest ready revision; `--no-traffic` pins it | ✅ verified |
| CR6 | a tag's URL is `trafficStatuses[].uri`, not to be parsed | ✅ verified |
| CR7 | short or full revision names in `latest*Revision` and `traffic` | ❓ (both accepted) |
| CR8 | `etag` in the body is a precondition; a stale one is refused (409 ABORTED in the sim) | ❓ (pinned) |
| CR9 | Operation shape; a template change's operation ends when the revision is ready, or with an error | shape ✅; timing ❓ (pinned; the adapter also waits for `Ready`) |
| CR10 | Revision `containers`, `conditions` with type Ready | ✅ verified |
| CR11 | after the operation, `traffic` and `trafficStatuses` are current | ✅ verified (`reconciling`) |
| CR12 | Cloud Run deletes non-addressable revisions past 1000 per service, untrafficked tags past 2000 per region | ✅ verified; whether a tag makes a revision addressable ❓ |
| CR13 | tags: lowercase DNS labels, unique | lowercase ✅; the rest ❓ |

## Secret sources

What each built-in secret source sends and reads (`packages/adapters/src/secrets.ts`). The CLI-backed ones are
tested with injected output and fake executables shaped like the documented output; the HTTP ones against a local
server and the generic REST sim (assumption R7's exact headers and fixed tokens) serving the documented answers.

| Source | Call | Verified | Notes |
| - | - | - | - |
| HashiCorp Vault | `GET $VAULT_ADDR/v1/{mount}/data/{path}`, `X-Vault-Token`, `X-Vault-Namespace` when `VAULT_NAMESPACE` is set; reads `data.data[key]` | ✅ | KV v2 "Read secret version"; without `?version=` the latest is returned. The token header and the namespace header are in the API overview. A 404 means "invalid path", which "can both mean that the path truly doesn't exist" or that the token may not see it, so the message says both. A 503 is a sealed or overloaded Vault (retried by the HTTP client as any GET). Errors are `{ "errors": [...] }` for any status ≥ 400 and carry no values. What a soft-deleted latest version answers (404 with metadata, or 200 with `data: null`) ❓; both are refused. The mount is the first segment: a mount whose path has a `/` cannot be named. |
| Infisical | `infisical secrets get NAME --projectId P --env E --path /F --output json --silent` | ✅ | Flags and `INFISICAL_TOKEN` / `INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN` from the CLI reference; `--silent` and `--domain` (`INFISICAL_DOMAIN`) are root flags (root.go). An unknown name: the CLI fetches the folder and, for a name it lacks, prints `*not found*` as its value and exits 0 (`getSecretsByNames`); with `--plain` it prints nothing. Hence JSON (`[{ secretKey, secretValue }]`) and `*not found*` refused. A secret whose real value is the literal `*not found*` cannot be told apart and is refused too. |
| Azure Key Vault | `az keyvault secret show --vault-name V --name N [--version X] --query value --output json` | ✅ | Parameters from the CLI reference (`--version`: "If omitted, uses the latest version"). Names checked against the documented rules: vault 3–24 of `0-9a-zA-Z` and non-consecutive `-`, secret 1–127 of `0-9a-zA-Z-`, version 32 characters. The CLI rather than REST: no Entra ID token step, and the CLI picks the vault DNS suffix of the signed-in cloud. A missing secret's exit status and text (`SecretNotFound`) ❓; any non-zero exit is `SECRET_UNRESOLVED` with the CLI's text. |
| 1Password Connect | `GET $OP_CONNECT_HOST/v1/vaults/{vault}/items/{item}`, `Authorization: Bearer $OP_CONNECT_TOKEN`; by name, first `GET /v1/vaults?filter=name eq "…"` and `GET /v1/vaults/{vault}/items?filter=title eq "…"` | ✅ | Paths, bearer header, filters (vaults by `name`, items by `title`), item `fields[]` (`id`, `label`, `value`, `section.id`) and `sections[]` (`id`, `label`) from the API reference; 401 invalid token, 403 no access, 404 not found. The reference calls ids UUIDs while its examples show 26-character lowercase strings; the source treats a 26-character `[a-z0-9]` segment as an id and anything else as a name, and compares names exactly itself, so a server that ignored `filter` would not change the result. `OP_CONNECT_HOST` (a URL such as `http://localhost:8080`) and `OP_CONNECT_TOKEN` are the variables 1Password's Connect SDKs, `op` and the Kubernetes operator read: ✅ ([connect-cli](https://developer.1password.com/docs/connect/connect-cli), [connect-sdk-go](https://github.com/1Password/connect-sdk-go)). |
| Bitwarden Secrets Manager | `bws secret get ID --output json --color no` | ✅ | Syntax, `json` as the default output, the `object, id, organizationId, projectId, key, value, note, creationDate, revisionDate` fields and `BWS_ACCESS_TOKEN` from the CLI docs; `BWS_SERVER_URL` and the UUID-typed id from `cli.rs`; one object, pretty-printed with a trailing newline, from `render.rs`. `--color no` because `auto` colours JSON whenever `supports_color` says so (`FORCE_COLOR` in CI). A missing id's exit status ❓; any non-zero exit is `SECRET_UNRESOLVED`. |

## What still needs a live account

* Vercel: V1 for a deployment (the list was checked), the deployment list's order, how overlapping target sets
  are refused, whether the env list pages with `until`, branch auto-cancel, and that a `gitSource` deployment from
  the project's `link` builds.

* Neon: N2 (whether any request answers `423` in practice, endpoint readiness).

* Clerk: C1 (status and wording of a duplicate), C2 (the envelope, confirmed only through the SDKs).

* LaunchDarkly: LD5 (re-adding a target the variation already serves; the status of a two-variation refusal), LD7
  (read-after-write), LD2 (an unknown environment), LD3 (the `user` placeholders in `contextTargets`).

* PlanetScale: PS4 (a new branch is not ready at first; how long provisioning takes), PS5 (status of a duplicate
  branch name), PS7 (deletion timing), PS9 (duplicate password names), PS12 (a password on a branch still
  provisioning), and PS1 and PS8 end to end.

* Supabase: S1 and S3 (a branch's ref is usable at once; the detail's status while it comes up), S4 (`db_pass` for a
  personal access token), S5 (status of a duplicate name), S6 (`404` for an unknown ref, list visibility after
  delete), S8 (verbatim storage of the list), S9 (a branch's own Auth config through the same endpoint). The
  Supabase block of the contract suite is optional in a live run (branching needs a paid plan).

* Netlify: NL2 (`name` is the subdomain), NL3 (PATCH leaves other values and keeps ids), NL5 (status of a
  duplicate key), NL6 (an emptied variable), NL7 (deploy order, `context` values, `review_id`), NL10 (`updated_at`
  on PATCH), NL11 (a build of a branch whose branch deploys are off).

* Cloudflare: CF3 (what a secret's `value` looks like in GET), CF4 (that PATCH merges the map), CF6 (status of an
  unknown project), CF7 (a write is readable at once).

* Algolia: AL5 (whether a copy's destination is visible before its task is published), AL6 (status of deleting a
  missing index), AL8 (how long a copy takes).

* Xata: XA3 (whether branch names are unique), XA5 (the spelling of `statusType`), XA6 (credentials of a branch still
  coming up), XA7 (how long a branch takes).

* DigitalOcean: DO3 (status of a duplicate name), DO6 (that the stored spec keeps every sent field in order), DO7
  (what a secret reads back as).

* Google Cloud Run: CR4 (status of a taken revision name), CR7 (short or full revision names), CR8 (whether a stale
  etag is refused, and how), CR9 (when a template change's operation ends), CR13 (tag rules beyond lowercase).

* Secret sources: Vault's answer for a soft-deleted latest version; the exact failure texts of `az`, `bws` and
  `infisical` for a missing secret.

* The generic adapter's authentication forms and GraphQL examples: every ❓ above, and each provider end to end
  (the examples run against sims, not the live APIs).

Run them with `pnpm test:live`; see the header of `scenarios/contract.test.ts`.

## Live runs

Runs of `pnpm test:live` (or a part of it) against real accounts. Every other check in this document is against
published specifications and documentation only.

| Date | What ran | Result |
| - | - | - |
| 2026-10-11 | `recipe:github.repo_webhook` against `api.github.com` (a public repository; the hook pointed at a reserved `example.com` name): create, re-apply, destroy | passed: re-apply wrote nothing (GH8 verified), destroy left no hook |
| 2026-10-11 | Vercel env vars and Neon branches, checked by hand against the real APIs through a logged-in dashboard session on throwaway projects (not `pnpm test:live`) | Vercel: V1 (list) and V4 verified, V3 contradicted (`decrypt=true` no longer decrypts; the per-variable GET does), V9 new (a `gitBranch` write without a connected repository is `400`). Neon: N1 and N5 verified, N2 contradicted in part (a second create meanwhile answered `201`), N6 new (a new project's default branch is `production`). Sim, adapters and the contract suite updated to match |
| 2026-10-11 | Release 0.6.1 through `scripts/publish-release.sh` | passed: all four packages carry SLSA v1 provenance (Sigstore transparency log), all four tags on origin; the first release with both |
| 2026-10-12 | First `pnpm test:live` run (live.yml) against the throwaway Vercel, Neon and Clerk projects | 67 of 68 passed (V1, V3, V4, V9, N1, N2, N5, N6, C2 hold on the real APIs); "second apply writes nothing" failed: Neon's create answer gives a connection string without `channel_binding=require` that GET /connection\_uri adds (N7, confirmed by hand), so the second apply rewrote the Vercel variable. Fixed by reading outputs only through GET /connection\_uri |
| 2026-10-12 | `pnpm test:live` again after the N7 fix | passed: 68 of 68 live (Vercel, Neon and Clerk), including "second apply writes nothing" |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.