# Change API

Request fields, responses, and merging for changes such as migrations, flag flips, and cache purges.

`POST /api/changes` puts a change that is not a deploy on the timeline: a database
migration, a feature flag flip, a cache purge, or a DNS change. It needs a bearer token
with the `deploy` scope, like the [Deploy API](https://docs.kogut.dev/deploys/api/).

## Request

```sh
curl -fsS -X POST https://kogut.acme.com/api/changes \
  -H "Authorization: Bearer $KOGUT_TOKEN" \
  -d title="Migrate users table" -d service=api
```

The body limit is 64 KiB. `Content-Type: application/json` selects JSON. Other content
types are parsed as form fields.

| Field | Required | Meaning |
|---|---|---|
| `title` | Yes | What changed, shown on the timeline. At most 200 characters. |
| `service` | No | Existing kogut service the change belongs to |
| `host` | No | Existing kogut host the change was made on |
| `by` | No | Person or system that made the change |
| `url` | No | Link shown with the change, such as the CI run or the flag |
| `description` | No | More detail shown with the change |

Optional fields allow at most 500 characters. Empty optional fields count as absent.
Unknown service or host names do not create them.

The change time is when kogut receives the request, so call it after the change takes
effect.

## Responses

Changes with the same title, service, and host within 2 min merge. A change without a
service or host merges only with another one without them. A new change returns `201`
with `{"event_id": 42, "merged": false}`. A merged request returns `200` with
`merged: true` and the first change's id.

| Status | Meaning |
|---|---|
| `201` | New change recorded |
| `200` | Merged with a change from the last 2 min |
| `400` | Unreadable body, unknown field, or JSON missing `title` |
| `401` | Missing or invalid bearer token |
| `403` | Token lacks the `deploy` scope |
| `413` | Body exceeds 64 KiB |
| `422` | Empty title, title over 200 characters, field over 500 characters, or unknown service or host |
| `503` | Server busy or storage wait exceeded. A retry can merge with a stored change. |

## Changes and deploys

Deploys and changes are stored before kogut answers and share one limit of 8 requests
waiting to be stored. Changes get no [deploy report](https://docs.kogut.dev/deploys/reports/).

A change counts as a [likely cause](https://docs.kogut.dev/incidents/correlation/) of an incident that starts
soon after it, under the `recent_change` rule. A change with neither a service nor a host
only ever coincides with an incident.

Changes appear on incident timelines, on the pages of their service and host, and in the
overview. A change with neither a service nor a host appears on every incident timeline in
its time and in the overview. The `incident` and `timeline` [MCP tools](https://docs.kogut.dev/reference/mcp/)
and [Copy for AI](https://docs.kogut.dev/incidents/copy-for-ai/) include them with author and link.

## Examples

A feature flag flip, as JSON, with a link to the flag:

```sh
curl -fsS -X POST https://kogut.acme.com/api/changes \
  -H "Authorization: Bearer $KOGUT_TOKEN" -H "Content-Type: application/json" \
  -d '{"title": "Turn on new checkout", "service": "web", "by": "ana", "url": "https://flags.acme.com/checkout"}'
```

A cache flush on one host:

```sh
curl -fsS -X POST https://kogut.acme.com/api/changes \
  -H "Authorization: Bearer $KOGUT_TOKEN" \
  -d title="Flush Redis cache" -d host=vps-1
```

A DNS change, with neither a service nor a host:

```sh
curl -fsS -X POST https://kogut.acme.com/api/changes \
  -H "Authorization: Bearer $KOGUT_TOKEN" \
  -d title="Move acme.com to new DNS provider" -d description="TTL 300"
```

## Shell example

The final `||` keeps a failed call from failing the job.

```sh title="migrate.sh"
#!/bin/sh
set -eu

./manage.py migrate
curl -fsS -X POST https://kogut.acme.com/api/changes \
  -H "Authorization: Bearer $KOGUT_TOKEN" \
  -d title="Migrate database" -d service=api -d by="$USER" \
  || echo "kogut: could not mark the change" >&2
```
