# API Reference
All endpoints return JSON. Error bodies have the shape `{"error": "<slug>", "message": "<human readable>"}`.
The full OpenAPI 3.0 spec is served at `GET /api-docs/openapi.json` and rendered at `GET /docs` (Swagger UI).
---
## POST /jobs
Enqueue a job. Returns the created (or, on idempotent retry, existing) job.
**Headers**
| `Content-Type: application/json` | yes | |
| `Idempotency-Key: <string>` | no | If supplied, repeated POSTs with the same key return the same job. Header takes precedence over `idempotency_key` in the body. |
**Body**
```json
{
"kind": "send_email",
"payload": {"to": "a@b.c", "subject": "hi", "body": "hello"},
"max_attempts": 3,
"idempotency_key": "user-42-welcome"
}
```
`kind` is one of: `send_email`, `resize_image`, `summarize_text`, `webhook_delivery`. The shape of `payload` must match the kind — see [src/payload.rs](../src/payload.rs).
**Responses**
| 201 Created | A new row was inserted. Body is the new job. |
| 200 OK | The idempotency key matched an existing row. Body is the existing job (same shape). |
| 422 Unprocessable Entity | `payload` is missing required fields for the kind. |
| 500 Internal Server Error | DB or serialization failure. |
**curl**
```bash
curl -i -X POST http://localhost:8080/jobs \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: user-42-welcome' \
-d '{
"kind": "send_email",
"payload": {"to": "a@b.c", "subject": "hi", "body": "hello"}
}'
```
---
## GET /jobs/{id}
Fetch a single job by id.
**Responses**
| 200 OK | Body is the job. |
| 404 Not Found | No row with that id. |
**curl**
```bash
curl -i http://localhost:8080/jobs/0190f9a0-1234-7000-8000-000000000001
```
---
## GET /jobs
List jobs in reverse-chronological order.
**Query**
| `status` | `queued \| running \| succeeded \| retrying \| failed_permanent \| cancelled` | _(all)_ | Exact match |
| `kind` | `send_email \| resize_image \| summarize_text \| webhook_delivery` | _(all)_ | Exact match |
| `limit` | int | 50 | Clamped to `[1, 200]` |
| `offset` | int | 0 | |
**Responses**
| 200 OK | JSON array of jobs |
**curl**
```bash
curl 'http://localhost:8080/jobs?status=succeeded&limit=10'
```
---
## POST /jobs/{id}/cancel
Request cancellation.
**Responses**
| 200 OK | The job was `queued` / `retrying`. The status is now `cancelled` (atomic transition). | `{"status": "cancelled", "job_id": "..."}` |
| 202 Accepted | The job is `running`. The `cancel_requested` flag is set; the worker will observe it at the next sub-step. | `{"status": "pending", "job_id": "..."}` |
| 404 Not Found | No such job. | error body |
| 409 Conflict | The job is already in a terminal state (`succeeded` / `failed_permanent` / `cancelled`). | error body |
**curl**
```bash
curl -i -X POST http://localhost:8080/jobs/0190f9a0-1234-7000-8000-000000000001/cancel
```
---
## GET /health
DB-level liveness check (`SELECT 1`).
**Responses**
| 200 OK | `{"status": "ok"}` |
| 500 | DB unreachable |
```bash
curl http://localhost:8080/health
```
---
## GET /metrics
Prometheus text-format metrics, rendered from the `metrics-exporter-prometheus` recorder installed at startup.
The API binary serves `/metrics` on the API port, usually `8080`. The worker binary serves its own `/metrics` endpoint on `WORKER_METRICS_BIND_ADDR`, usually `0.0.0.0:9091`, and emits worker counters/histograms such as `worker_jobs_started_total`, `worker_jobs_completed_total`, and `worker_job_duration_seconds`.
```bash
curl http://localhost:8080/metrics
curl http://localhost:9091/metrics
```
---
## GET /docs
Swagger UI rendered from `ApiDoc` (utoipa-generated). Open in a browser.
## GET /api-docs/openapi.json
OpenAPI 3.0 JSON spec.
```bash
---
## Error shape
Any 4xx/5xx response uses this body:
```json
{
"error": "validation",
"message": "payload validation failed for send_email: missing field `body`"
}
```
The `error` slug is one of: `not_found`, `bad_request`, `conflict`, `validation`, `internal`.
For 500 responses the `message` is redacted to `"internal server error"`; full detail is in the structured tracing logs.