HTTP API

The asyncjobs library can be exposed over an HTTP/JSON API so non-Go services and operators can enqueue tasks, manage queues, and inspect schedules without linking the library. The contract is OpenAPI 3.0; the canonical document lives at api/openapi.yaml.

The server is shipped as the httpserver subpackage and a launchable ajc server run subcommand.

Quickstart

Start the server on loopback and call it with curl:

$ ajc queue add EMAIL --run-time 1h
$ ajc server run --queue EMAIL
INFO[0000] asyncjobs HTTP API listening on 127.0.0.1:8080 (none)
$ curl -sf http://127.0.0.1:8080/v1/info
{"version":"v0.1.0","auth":"none","queue_count":1,"task_count":0}

ajc server run flags

FlagDefaultNotes
--bind127.0.0.1:8080Listen address. Non-loopback requires --unsafe-bind or mTLS.
--tls-cert, --tls-key(none)Enable TLS. Both must be supplied together.
--tls-client-ca(none)CA bundle used to verify client certificates. Enables mTLS.
--queue(none)Work queue the API enqueues to. Required unless --allow-create-default.
--allow-create-defaultfalsePermit auto-creating the implicit DEFAULT queue.
--read-timeout30sPer-request read timeout.
--write-timeout30sPer-request write timeout.
--max-body524288Maximum request body size, in bytes (default 512 KiB).
--unsafe-bindfalseAcknowledge that a non-loopback bind exposes an unauthenticated server.

The server binds to a dedicated http.ServeMux, never the default mux, and applies a MaxBytesReader to every request body. Request bodies and the Authorization header are never logged.

Authentication

The server performs no authentication and no authorization. The default loopback bind is sufficient for same-host integrations; anything else must be fronted by a reverse proxy or secured with mTLS.

A non-loopback --bind is rejected at startup unless one of the following holds:

  • Reverse proxy (nginx, oauth2-proxy, envoy, caddy, Traefik, Tailscale, …): pass --unsafe-bind to acknowledge that you have fronted the server with a proxy that terminates authentication.
  • mTLS: pass --tls-cert, --tls-key, and --tls-client-ca. When a client-CA bundle is configured, every TLS client must present a certificate chain that verifies against it. Certificate subjects are not mapped to scopes; successful verification alone grants full access.

/v1/info advertises the mode as auth: "none" or auth: "mtls" so callers and health checks can assert it externally.

Reverse-proxy requirements

Any fronting proxy must:

  • Terminate authentication (bearer token, OIDC, SSO, mTLS, whatever fits) before reaching the asyncjobs listener.
  • Preserve or strip request headers per the proxy’s own policy; asyncjobs itself reads none.
  • Pass through 413 Payload Too Large and 429 Too Many Requests emitted by the backend.

Client requests rejected by the proxy receive the proxy’s error envelope (a 401 or 403), not the asyncjobs envelope — those status codes do not originate from this server.

Rotating mTLS material

There is no hot-reload for TLS or client-CA material; restart the server to pick up new files.

Endpoint catalog

All paths are versioned under /v1.

Meta

MethodPathNotes
GET/healthzLiveness probe. Does not touch storage.
GET/readyzReadiness probe. Returns the same body shape on 200/503.
GET/v1/openapi.jsonThe full OpenAPI document, generated from the embedded YAML.
GET/v1/infoServer version, auth mode, queue count, task count.
GET/v1/retry-policiesThe known retry policies and their intervals.

Tasks

MethodPathNotes
POST/v1/tasksEnqueue a task. Honors Idempotency-Key.
GET/v1/tasksSnapshot list. Supports limit, state, queue, type, created_since, stream=ndjson. Rate-limited.
GET/v1/tasks/{id}Fetch a task.
DELETE/v1/tasks/{id}Remove a task from storage.
POST/v1/tasks/{id}/retryRetry a single task by id.
POST/v1/tasks/retryBulk retry, capped at 100 ids; returns per-item results.

Queues

MethodPathNotes
POST/v1/queuesCreate a queue.
GET/v1/queuesList queues. Raw JetStream and consumer detail are never returned.
GET/v1/queues/{name}Queue detail.
DELETE/v1/queues/{name}Delete a queue.
POST/v1/queues/{name}/purgePurge all entries.

Schedules

MethodPathNotes
POST/v1/schedulesCreate a scheduled task.
GET/v1/schedulesList schedules.
GET/v1/schedules/{name}Schedule detail.
DELETE/v1/schedules/{name}Remove a schedule.

Error envelope

Every error response shares the same shape:

{
  "error": {
    "code": "not_found",
    "message": "task not found: 24YUZF...",
    "details": {
      "reason": "task_not_found"
    }
  }
}

code is a stable string drawn from a closed enum (invalid_argument, not_found, conflict, duplicate, rate_limited, payload_too_large, unavailable, signature_required, signature_invalid, dependency_failed, internal). Treat code as the machine-readable surface; message is for operators.

details.reason carries a library-specific identifier (e.g. task_not_found, queue_already_exists, payload_both_set) and lets clients branch on a finer-grained signal than code alone provides. details.field is set on validation failures to the offending field name.

Quirks worth remembering

Payload encoding. Create requests accept either payload (any JSON value, server-encoded) or payload_base64 (pre-encoded bytes). Setting both yields HTTP 400 with details.reason: payload_both_set. Task and schedule responses always return payload as a base64 string; this matches the library’s []byte JSON encoding. Decode before use, and use payload_base64 when round-tripping a fetched task.

Duration vs RFC3339 dates. Queue duration fields (max_age, max_runtime) accept Go duration strings (30s, 5m) on requests but echo int64 nanoseconds on responses. Task deadline is an absolute RFC3339 timestamp. Schedule deadline_offset is a Go duration string applied at fire-time. The names differ deliberately so the schemas cannot silently collide.

Rate-limited list. GET /v1/tasks opens a fresh ephemeral JetStream consumer per call and is serialized server-side. Concurrent callers receive HTTP 429 (code: "rate_limited"). stream=ndjson tail mode is capped at roughly 60 seconds by the underlying library; clients should reconnect.

Default-queue guard. POST /v1/queues rejects name: "DEFAULT" unless the server was started with ajc server run --allow-create-default. The library would otherwise auto-create it, masking misconfigured deployments.

Signing is the caller’s responsibility. When the deployment has a TaskVerificationKey configured, the HTTP server does not sign on the caller’s behalf. Callers must supply a pre-computed signature field.

Idempotency. POST /v1/tasks honors the Idempotency-Key header for a 10-minute window: a repeat request with the same key returns the existing task as 200 with the same Location header rather than enqueuing twice.

Generating client SDKs

The OpenAPI document at /v1/openapi.json is OpenAPI 3.0.3 — emitted from the embedded YAML at server boot. Use a generator that supports 3.0:

openapi-generator-cli generate \
  -i http://127.0.0.1:8080/v1/openapi.json \
  -g python \
  -o ./client

For the openapi-generator-cli tool, pin the input version with --spec-version 3.0.3 if your generator defaults to 3.1; the asyncjobs spec does not use 3.1-only constructs but generators may emit warnings otherwise.

The repository also exposes the YAML directly at api/openapi.yaml for offline generation.