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

# Report changes from your CI — deploys, prompts, models and flags on your timeline

> Tell Zespan from your pipeline (or your app at boot) that a deploy, prompt, model, config or flag changed, with one call that sends metadata only. Shows on charts and the Changes page and gives a finding the right change to name.

When something breaks, the first question is what changed. Zespan already knows about prompt deploys, agent promotions and policy edits. It cannot see your pipeline's deploys, a model switch in your config or a feature flag flip unless you tell it. One short call from CI does that, and the change then shows as a marker on your charts, on the [Changes](https://docs.zespan.com/dashboard/changes.md) page and, where it fits, as the change a finding points to.

The call sends **metadata only**: what kind of change, which service and environment, a short commit id, your version label and a time. It has no field for code, a diff, a commit message, a branch, a file name or a person. If you send an email address or an attribute named like one, Zespan refuses it.

**1. Create a change key**

In the dashboard go to **Settings → API keys → Change keys** and create a key (named for the pipeline, for example `GitHub Actions`). Copy it now: it is shown once. You need the owner or admin role.

**2. Store it as a CI secret**

Save it as `ZESPAN_CHANGE_KEY` in your CI provider's secret store. Do not use your project key for this: a change key can only report changes, so a leaked pipeline secret cannot read your data or run anything that costs money.

**3. Add one step after your deploy**

Pick the snippet for your pipeline below. Run it after the deploy succeeds.

## Add the step

> [tab] GitHub Actions
>
> ```yaml theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
> - name: Report the deploy to Zespan
>   run: >
>     npx -y @zespan/cli change --kind deploy --service checkout --env production
>     --ref "$GITHUB_SHA" --version v2.4.1
>   env:
>     ZESPAN_CHANGE_KEY: ${{ secrets.ZESPAN_CHANGE_KEY }}
> ```
>
> GitHub has no default variable for the environment name or your release label, so write them in (`production` and `v2.4.1` above). Do not use `github.ref_name`: on a push it is the branch name.

> [tab] GitLab CI
>
> ```yaml theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
> report_deploy:
>   stage: .post
>   script:
>     - >
>       npx -y @zespan/cli change --kind deploy --service checkout
>       --env "$CI_ENVIRONMENT_NAME" --ref "$CI_COMMIT_SHORT_SHA" --href "$CI_PIPELINE_URL"
>   variables:
>     ZESPAN_CHANGE_KEY: $ZESPAN_CHANGE_KEY
> ```
>
> > [!WARNING]
> >
> > Never pass `CI_COMMIT_AUTHOR`, `GITLAB_USER_EMAIL` or `CI_COMMIT_MESSAGE`. They are personal data or free text, and Zespan does not want them.

> [tab] curl
>
> ```bash theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
> curl -sS -X POST "${ZESPAN_URL:-https://api.zespan.com}/v1/changes" \
>   -H "x-api-key: $ZESPAN_CHANGE_KEY" \
>   -H "content-type: application/json" \
>   -d '{"kind":"deploy","service":"checkout","environment":"production","ref":"'"${GIT_SHA:0:12}"'","version":"v2.4.1"}' || true
> ```
>
> The trailing `|| true` keeps your pipeline green if Zespan cannot be reached.

> [tab] From your app
>
> Report a release when your app starts. Many processes starting with the same release store one change.
>
> ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
> import { zespan } from "@zespan/sdk";
>
> await zespan.change({ kind: "deploy", service: "checkout", version: process.env.APP_VERSION });
> ```
>
> ```python Python theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
> import os
> import zespan
>
> zespan.change("deploy", service="checkout", version=os.environ.get("APP_VERSION"))
> ```
>
> `change()` never throws: it returns `{ ok, id, replayed, reason }` (Python: a `ChangeResult`). It reads `ZESPAN_CHANGE_KEY` first, then your SDK key, and sends nothing when `ZESPAN_CHANGES=off`.

> [!TIP]
>
> A deploy is the most useful change to report. If you can only add one thing, add that, with a `--service` that matches your agent or service name.

## What you can send

| Field | CLI flag | What it is |
| - | - | - |
| `kind` | `--kind` | `deploy`, `prompt`, `model`, `config`, `flag`, `dependency` or `other` (required) |
| `service` | `--service` | The service that changed. Letters, digits and `. _ / -` |
| `environment` | `--env` | `production`, `staging` and so on |
| `ref` | `--ref` | A commit-style id, 7 to 40 hex characters. Zespan keeps the first 12 |
| `version` | `--version` | Your version label, up to 64 characters |
| `at` | `--at` | When it happened, UTC like `2026-10-24T09:00:00Z`. Defaults to now. At most 7 days back |
| `title` | `--title` | A short title. Generated when left out, for example `Deploy of checkout v2.4.1 (a1b2c3d4e5f6) to production` |
| `href` | `--href` | A link, for example your CI run |
| `attributes` | `--attr key=value` | Up to 10 keys you choose, with short string, number or boolean values |
| `prompt` | `--prompt` | For a prompt change: the prompt's name |
| `idempotencyKey` | `--idempotency-key` | Your own key. Usually not needed |

> [!WARNING]
>
> Do not send commit messages, author names or emails, branch names, file names or diffs. There is no field for them. An email address in any text field, or an attribute named `author`, `author_email`, `author_name`, `committer`, `email`, `user`, `username`, `message`, `commit_message`, `files`, `file`, `diff`, `patch` or `branch`, is rejected.

## Reporting twice is safe

A change with a `ref` or a `version` gets a default idempotency key built from its kind, service, environment, ref and version. Send the same release again, because a job retried or fifty pods started together, and Zespan keeps one change: the repeat answers `200` with `replayed: true`. Changes without a ref or version are not deduplicated, so a second call stores a second change.

Each project accepts 60 change reports a minute and 2,000 a day, and a request body up to 16 KB.

## Your pipeline never fails because of Zespan

The CLI exits `0` and prints a warning when Zespan cannot be reached or your key is rejected, so a Zespan outage cannot block a deploy. Add `--fail-on-error` if you want a non-zero exit instead. Invalid input (a missing `--kind`, an email in a field) exits `2`.

| Exit code | Meaning |
| - | - |
| `0` | Reported, or not delivered with a warning |
| `1` | Not delivered, and you passed `--fail-on-error` |
| `2` | Invalid input; nothing was sent |

`zespan change --dry-run` prints what would be sent and sends nothing.

## What Zespan does with it

* **Markers on your charts.** Changes show as dashed markers on the Overview, Performance and Errors charts and as rows on the [Changes](https://docs.zespan.com/dashboard/changes.md) page, with service, environment and version.
* **A named change in findings.** When a finding starts at the time of a change you reported, the finding can name it ("a deploy of checkout v2.4.1, reported by your CI") and its evidence links to the change's time. A change reported by your CI is your statement, not something Zespan observed, so on its own it never raises a finding above "likely". When Zespan also sees the new version appear in your traces, the two are combined into one finding with a before-and-after comparison, and that comparison is what can make it stronger. Use the same `--service` name your SDK reports as the service name, and the same `--version`, so the two match.
* **Context for drift.** A behaviour drift check lists a coinciding change, with the words "Coincidence is not proof of cause".
* **A guess at the layer, never a decision.** A prompt change points at the instructions layer, a model change at the model layer, and so on, always as a possibility. A change you report is never treated as proof of where a failure started.

> [!NOTE]
>
> Findings and drift context that use your reported changes appear once this is turned on for your project; until then, the findings you see do not change. Markers and the Changes page work from the start. On a self-hosted deployment, `CHANGE_CI_ATTRIBUTION=on` turns it on and `off` turns it off.

Reported changes are kept for 400 days.

## Change keys

A change key (`zsc_...`) can report changes and nothing else: every other route answers `403` with `change_key_scope`. Create and revoke keys under **Settings → API keys → Change keys** (owner or admin). A revoked key stops working on the next request. The list shows each key's first 12 characters and when it was last used, never the key.

> [!NOTE]
>
> Your project key also works for `POST /v1/changes`, but it can do much more than report a change. Use a change key in CI.

## Next steps

* [Changes](https://docs.zespan.com/dashboard/changes.md) — the timeline, filters and the typed fields of the API
* [CLI overview](https://docs.zespan.com/cli/overview.md) — the other `zespan` commands
* [TypeScript SDK](https://docs.zespan.com/sdk/typescript.md#report-changes) and [Python SDK](https://docs.zespan.com/sdk/python.md#report-changes) — `change()` in your app
* [Incidents](https://docs.zespan.com/dashboard/incidents.md) — the before and after change panel


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