API reference

The stafett-enqueue image serves this HTTP API on port 8000. In Kubernetes the base URL is the Service, e.g. http://stafett-worker-enqueue.stafett:8000. In the quickstart it is http://localhost:8000.

There is no authentication. Anyone who can reach the port can enqueue a POST to any URL, so keep it off the public internet and limit who can reach it inside your network.

FastAPI serves the schema at /openapi.json and an explorer at /docs on the same port.

Enqueue

POST /enqueue

The request body is the payload, sent to the destination byte for byte. Everything else is a request header.

curl -X POST http://localhost:8000/enqueue \
  -H 'content-type: application/json' \
  -H 'stafett-url: https://hooks.example.com/orders' \
  -H 'stafett-dedupe-key: order-42' \
  -H 'stafett-header-authorization: Bearer …' \
  --data-binary '{"order": 42}'
Part Required Contract
Body no Any UTF-8 text, at most 1 MiB. Stafett never parses or re-encodes it. Empty by default.
Content-Type no Sent as the delivery's content-type. Without it, none is sent.
Stafett-Url yes http or https, at most 2,048 characters.
Stafett-Header-<name> no Sent as <name>. Up to 50, counting Content-Type. Each value at most 4,096 characters. See "Headers" below.
Stafett-Delay-Seconds no Wait this long before the first attempt. Non-negative integer, default 0.
Stafett-Dedupe-Key no 1 to 256 characters. While a job with this key is queued or in flight, another enqueue with it answers 409.

Other request headers, such as your client's user-agent, are not sent on. An unknown or repeated Stafett-* header is refused with 422, so a typo cannot silently drop an option.

Answers:

Status Meaning
202 Committed. The job is in Postgres and will be delivered.
409 A job with this Stafett-Dedupe-Key is queued or in flight. Nothing was written.
422 The request breaks a limit above. Nothing was written.
503 The database is unavailable. See below.
{"id": 1234, "status": "queued", "status_url": "/jobs/1234"}

id is an integer, unique within one database.

A 503 is almost always "nothing written". The exception: if the connection drops during the commit itself, the job may have been committed. Retrying with a Stafett-Dedupe-Key answers 409 if the first attempt landed and that job is still queued or in flight. If it was already delivered or dead, the retry is accepted as a new job, with a new webhook-id when signing is on.

Enqueue is not part of your transaction. If your service commits its own data and then crashes before calling POST /enqueue, the job is never queued. Call enqueue after your commit and retry it until you get 202 or 409.

Job status

GET /jobs/{id}

{"id": 1234, "status": "retrying", "attempts": 2, "created_at": "2026-11-03T09:00:00Z"}
Status Meaning
queued Waiting for its first attempt.
delivering A deliver pod is running an attempt now.
retrying At least one attempt failed; waiting for the next.
delivered The destination answered 2xx.
dead Not retryable, or out of attempts. Kept for replay.
canceled Canceled before it was delivered.

attempts counts every attempt over the job's life, replays included, so it only ever goes up. Unknown id: 404. Database unavailable: 503.

Cancel

DELETE /jobs/{id}

Answers 204 for any job that exists. A job that is queued or retrying becomes canceled and is never sent. A job that is delivering, or already finished, is left alone: check GET /jobs/{id} for what happened. Unknown id: 404.

Health

GET /healthz

{"status": "ok", "version": "0.1.0"}

Served by both images. On deliver it answers 503 once the delivery loop has stopped, so a liveness probe restarts it.

The delivery request

deliver sends a POST to Stafett-Url. You decide the body and the headers; Stafett adds only what you left out, and signs the request if deliver has a signing secret.

With signing on (STAFETT_SIGNING_SECRET set):

POST <url>
content-type: application/json
user-agent: Stafett/0.1.0 (+https://stafett.dev)
<your headers>
webhook-id: 019a4b7c-6c4a-7b32-9f21-2c0a5e4d9b11
webhook-timestamp: 1793350800
webhook-signature: v1,<base64 HMAC-SHA256>

{"order":42}

With signing off, the same request without the three webhook-* lines.

Headers

Each Stafett-Header-<name> is sent as <name>, lower-cased, with its value as given. That includes authorization, user-agent, content-type, host, x-forwarded-*, and, with signing off, your own webhook-* signature headers.

Only what HTTP cannot carry is refused at enqueue with 422, and dropped at delivery if a job got around enqueue:

Headers are stored with the job in your database until it is delivered or removed, so a token in authorization sits in the stafett schema meanwhile.

Signing it yourself. With deliver's signing off, you can send your own signature headers. If your scheme includes a timestamp, remember a job can go out well after you signed it: a Stafett-Delay-Seconds, a backlog, a database outage, retries, or a replay from the dead-letter queue. A receiver that rejects old timestamps (Standard Webhooks allows 5 minutes) answers 4xx, and the job is dead. Static credentials, such as a bearer token or an API key, never expire this way.

Retries

Destination answer Result
2xx delivered
429, 5xx, timeout, connection error Retry after backoff, while attempts remain; otherwise dead.
Any other status, including 3xx and 4xx dead at once.

Each failed attempt records a reason in Postgres: HTTP <code>, timeout, connect error, unsafe url, unknown envelope version, or missing envelope (a queue row with no request, written around enqueue). Operations shows how to read it.

Delivery is at-least-once. A job whose attempt timed out after the destination processed it is sent again. Make receivers dedupe on webhook-id, or, with signing off, on an id header of your own.