Skip to main content

Jobs API

The Jobs API lets you create background jobs, query their status, cancel pending work, and manually retry failures. These endpoints are called by the SDK's IJobClient and can also be used directly via HTTP.

Base URL: https://api.zeridion.com/flare/v1

All endpoints require Bearer token authentication and are subject to rate limits.


POST /flare/v1/jobs

Create a new background job. The job is persisted and will be picked up by one of your workers for execution.

Request

POST /flare/v1/jobs
Authorization: Bearer <api_key>
Content-Type: application/json
Idempotency-Key: welcome-email-user-42

The Idempotency-Key request header is the RFC-style replay primitive — same key + same body returns the original 201 Created plus Idempotent-Replay: true. Same key + a different body returns 409 idempotency_key_reuse. The legacy body-field idempotency_key shown below is retained for backwards compatibility but is deprecated; new clients should prefer the header.

Body

FieldTypeRequiredDescription
job_typestringYesJob type identifier, typically the fully qualified class name. Max 500 characters.
payloadobjectYesJSON payload passed to the job at execution time.
queuestringNoTarget queue name. Max 100 characters. Default: "default".
run_atstring (ISO 8601)NoSchedule the job for future execution. Omit for immediate.
max_attemptsintegerNoMaximum execution attempts (1–100). Default: 3.
timeout_secondsintegerNoPer-attempt timeout (1–86,400). Default: 1800 (30 minutes).
idempotency_keystringNoUnique key for deduplication. Max 200 characters.
tagsobjectNoKey-value string pairs (Dictionary<string, string>) visible in the dashboard. Both keys and values must be strings.
parent_job_idstringNoCreate as a continuation of this parent job. Max 36 characters.

Full example

{
"job_type": "email.send",
"payload": {
"to": "user@example.com",
"subject": "Welcome!",
"body": "Thanks for signing up."
},
"queue": "email",
"max_attempts": 5,
"timeout_seconds": 60,
"idempotency_key": "welcome-email-user-42",
"tags": {
"env": "dev",
"priority": "high"
}
}

Minimal example

{
"job_type": "report.generate",
"payload": { "report_id": 1 }
}

Response

201 Created

Returns a Location header with the job URL: Location: /flare/v1/jobs/\{id\}

{
"id": "job_01HYX3K7M8N9P2Q4R5S6T7U8V9",
"state": "pending",
"job_type": "email.send",
"queue": "email",
"created_at": "2026-03-18T15:30:00Z",
"run_at": null,
"attempt": 0,
"max_attempts": 5
}
FieldTypeDescription
idstringUnique job identifier
statestringInitial state: pending (immediate) or scheduled (has run_at or parent_job_id)
job_typestringEchoed job type
queuestringResolved queue name
created_atstring (ISO 8601)Timestamp of creation
run_atstring (ISO 8601)Scheduled execution time, or null for immediate
attemptintegerCurrent attempt number (starts at 0)
max_attemptsintegerMaximum attempts allowed

Errors

StatusCodeCondition
400invalid_requestValidation failed (see validation rules)
402billing_state_blockedProject's billing state is not Active (e.g. PastDue, Cancelled, Suspended). Resolve via the dashboard billing portal. Applies to every non-billing endpoint, including GET /flare/v1/jobs.
409idempotency_key_reuseThe idempotency_key was previously used with a different request body. Same-key + same-body replays the original 201 instead.
409parent_terminalParent job is cancelled or dead_letter
422parent_not_foundparent_job_id does not reference an existing job

Idempotent replay. When the same idempotency_key is sent with the same body — or when an Idempotency-Key request header is reused — the server returns the original 201 Created response verbatim, plus an Idempotent-Replay: true response header. New clients should prefer the Idempotency-Key header; the body-field idempotency_key is retained for backwards compatibility and will be removed in /flare/v2.


GET /flare/v1/jobs/{job_id}

Retrieve full details for a single job, including payload, timing, error information, and tags.

Request

GET /flare/v1/jobs/{job_id}
Authorization: Bearer <api_key>

Response

200 OK

{
"id": "job_01HYX3K7M8N9P2Q4R5S6T7U8V9",
"state": "succeeded",
"job_type": "email.send",
"queue": "email",
"payload": { "to": "user@example.com", "subject": "Welcome!" },
"created_at": "2026-03-18T15:30:00Z",
"started_at": "2026-03-18T15:30:01Z",
"completed_at": "2026-03-18T15:30:02Z",
"attempt": 1,
"max_attempts": 5,
"progress": 1.0,
"duration_ms": 1042,
"error": null,
"tags": { "env": "dev", "priority": "high" }
}
FieldTypeDescription
idstringUnique job identifier
statestringCurrent job state: pending, scheduled, processing, succeeded, failed, cancelled, dead_letter
job_typestringJob type identifier
queuestringQueue the job belongs to
payloadobjectThe job's JSON payload
created_atstring (ISO 8601)When the job was created
started_atstring (ISO 8601)When execution began, or null
completed_atstring (ISO 8601)When execution finished, or null
attemptintegerCurrent attempt number
max_attemptsintegerMaximum attempts allowed
progressnumberProgress value (0.0–1.0) reported by the job, or null
duration_msintegerExecution duration in milliseconds, or null
errorobjectError details if the job failed (see below), or null
tagsobjectKey-value string pairs (Dictionary<string, string>), or null

Error detail object

When a job fails, the error field contains:

FieldTypeDescription
typestringException type name
messagestringError message
stack_tracestringStack trace from the failed attempt

Errors

StatusCodeCondition
404job_not_foundJob does not exist or belongs to a different project

GET /flare/v1/jobs

List jobs with optional filtering and cursor-based pagination.

Request

GET /flare/v1/jobs?state=failed&queue=email&limit=20
Authorization: Bearer <api_key>

Query Parameters

ParamTypeDefaultDescription
statestringallFilter by state: pending, scheduled, processing, succeeded, failed, cancelled, dead_letter
queuestringallFilter by queue name
job_typestringallFilter by job type
created_afterstring (ISO 8601)Only return jobs created after this timestamp
created_beforestring (ISO 8601)Only return jobs created before this timestamp
limitinteger50Number of results per page (1–100)
cursorstringCursor from a previous response's next_cursor for the next page

Response

200 OK

{
"data": [
{
"id": "job_01HYX3K7M8N9P2Q4R5S6T7U8V9",
"state": "failed",
"job_type": "email.send",
"queue": "email",
"payload": { "to": "user@example.com" },
"created_at": "2026-03-18T15:30:00Z",
"started_at": "2026-03-18T15:30:01Z",
"completed_at": "2026-03-18T15:30:02Z",
"attempt": 3,
"max_attempts": 3,
"progress": null,
"duration_ms": 503,
"error": {
"type": "System.Net.Http.HttpRequestException",
"message": "Connection refused",
"stack_trace": " at System.Net.Http..."
},
"tags": null
}
],
"has_more": true,
"next_cursor": "eyJpZCI6ImpvYl8wMUhZWDNLN004TjlQMlE0UjVTNlQ3VThWOSJ9"
}
FieldTypeDescription
dataarrayArray of job detail objects
has_morebooleantrue if more results exist beyond this page. When false, next_cursor is always null.
next_cursorstringPass as the cursor query parameter to fetch the next page. null exactly when has_more is false — the two fields are equivalent end-of-stream signals, treat either one as authoritative.

Sort order

Results are sorted by created_at descending (newest first), with ties broken by id descending.

Pagination

The API uses cursor-based pagination to provide consistent results under concurrent writes. To paginate through all results:

  1. Make the initial request without a cursor
  2. If has_more is true, use the next_cursor value as the cursor parameter in the next request
  3. Repeat until has_more is false

:::info Treat cursors as opaque The next_cursor value is an opaque string — do not parse, decode, or construct cursor values on the client side. The current encoding (base64 JSON) is an implementation detail and may change without notice. Always pass the exact string the server returned in next_cursor straight back as the next request's cursor parameter. :::

# Page 1
curl "https://api.zeridion.com/flare/v1/jobs?state=failed&limit=20" \
-H "Authorization: Bearer $API_KEY"

# Page 2 (using next_cursor from page 1)
curl "https://api.zeridion.com/flare/v1/jobs?state=failed&limit=20&cursor=eyJ..." \
-H "Authorization: Bearer $API_KEY"

POST /flare/v1/jobs/{job_id}/cancel

Cancel a job. Only jobs in pending or scheduled state can be cancelled.

Request

POST /flare/v1/jobs/{job_id}/cancel
Authorization: Bearer <api_key>

No request body is required.

Response

200 OK

{
"id": "job_01HYX3K7M8N9P2Q4R5S6T7U8V9",
"state": "cancelled"
}

Behavior

  • Pending / Scheduled jobs are cancelled immediately.
  • Processing, succeeded, failed, cancelled, and dead-letter jobs cannot be cancelled — the endpoint returns 409 Conflict.
  • Parent cancellation cascades: cancelling a parent job also cancels any child continuation jobs that are still in scheduled state.

Errors

StatusCodeCondition
403forbiddenCaller's project-membership role is below member (cancellation requires RoleRank >= member)
404job_not_foundJob does not exist
409invalid_stateJob is not in pending or scheduled state

POST /flare/v1/jobs/{job_id}/retry

Manually retry a failed or dead-lettered job. Resets the job to pending state so it can be picked up by a worker again.

Request

POST /flare/v1/jobs/{job_id}/retry
Authorization: Bearer <api_key>

No request body is required.

Response

200 OK

{
"id": "job_01HYX3K7M8N9P2Q4R5S6T7U8V9",
"state": "pending",
"attempt": 3
}
FieldTypeDescription
idstringJob identifier
statestringAlways pending after a successful retry
attemptintegerCurrent attempt number

Behavior

  • Only jobs in failed or dead_letter state can be retried.
  • If the job has exhausted all attempts (attempt >= max_attempts), the max_attempts is automatically bumped to attempt + 1 to allow the retry.
  • The job's error details, timing fields, and worker assignment are cleared.

Errors

StatusCodeCondition
404job_not_foundJob does not exist
409invalid_stateJob is not in failed or dead_letter state

Job Continuations

Continuations let you chain jobs together in a parent-child relationship. A child job waits for its parent to succeed before it runs.

Creating a continuation

Include parent_job_id when creating a job via POST /flare/v1/jobs:

{
"job_type": "notification.send",
"payload": { "channel": "slack", "message": "Data sync complete" },
"queue": "default",
"parent_job_id": "job_01HYX3K7M8N9P2Q4R5S6T7U8V9"
}

State behavior

The child's initial state depends on the parent's current state:

Parent stateChild initial stateBehavior
pending / scheduled / processingscheduledChild waits for parent to complete
succeededpendingChild runs immediately (parent already done)
cancelled / dead_letterCreation rejected with 409 parent_terminal

Activation and cascading

  • Parent succeeds: all child jobs in scheduled state are activated (moved to pending). The parent's completion ack response includes children_activated with the count.
  • Parent dead-letters: all child jobs in scheduled state are cancelled.
  • Parent is cancelled: all child jobs in scheduled state are cancelled.

Errors

StatusCodeCondition
409parent_terminalParent is cancelled or dead_letter
422parent_not_foundThe parent_job_id does not reference an existing job

Server-Sent Events: GET /flare/v1/jobs/{job_id}/events

Subscribe to a live progress stream for a single job. The server emits snapshot events approximately once per second whenever the job's state or progress changes, then closes the stream when the job reaches a terminal state or after 120 seconds — whichever comes first. The browser EventSource (or any SSE client) will automatically reconnect and receive the current snapshot on the next tick.

Request

curl -N "https://api.zeridion.com/flare/v1/jobs/job_01HYX3K7M8N9P2Q4R5S6T7U8V9/events" \
-H "Authorization: Bearer $API_KEY"

The -N flag disables curl's output buffering so frames appear as they arrive rather than being held until the stream closes.

Response

200 OK with Content-Type: text/event-stream

Each SSE frame uses the snapshot event name and carries a JSON data object:

event: snapshot
data: {"state":"processing","progress":0.42,"attempt":1,"max_attempts":3}

event: snapshot
data: {"state":"succeeded","progress":1.0,"attempt":1,"max_attempts":3}

Data fields

FieldTypeDescription
statestringCurrent job state (pending, processing, succeeded, failed, cancelled, dead_letter)
progressnumberProgress in [0.0, 1.0] reported by the job, or null
attemptintegerCurrent attempt number
max_attemptsintegerMaximum attempts allowed

Connection lifecycle

  1. First tick: the server validates the job exists (returns 404 if not) and immediately emits the current snapshot without waiting for a change.
  2. Subsequent ticks: a new snapshot is emitted only when state or progress differs from the previous tick, keeping idle connections cheap.
  3. Terminal states: the stream closes automatically when state reaches succeeded, failed, cancelled, or dead_letter.
  4. 120-second deadline: the server closes the stream unconditionally after 120 seconds to prevent stale connections. Reconnect to resume.

Reconnection guidance

EventSource reconnects automatically after the server closes or drops the connection. On reconnect the first snapshot reflects the current job state, so your UI can diff against the last-known state. If you need the complete job object (payload, tags, error details) after a terminal event, fetch GET /flare/v1/jobs/{job_id} once the stream closes.

:::note Infrastructure requirement Container Apps ingress timeout must be ≥ 125 seconds so the 120-second stream is not truncated mid-connection. See the internal operations runbook for the ingress configuration. :::

Errors

StatusCodeCondition
404job_not_foundJob does not exist or belongs to a different project

GET /flare/v1/jobs/export

Bulk-export all jobs for your project within a date range, streamed directly to a file. Useful for offline analysis, reconciliation, and compliance snapshots.

Rate limit. Exports are throttled to 1 request per minute per project. A second call inside the cooldown returns 429 rate_limit_exceeded with Retry-After. The export window is also capped at 90 days — requests with a wider span return 400 range_too_large.

Authentication: Bearer API key. Tenant-scoped — only jobs belonging to the authenticated project are returned.

Request

GET /flare/v1/jobs/export?from=<iso>&until=<iso>&format=jsonl|csv
Authorization: Bearer <api_key>

Query parameters

ParameterTypeRequiredDescription
fromstringyesISO 8601 start of the export window (inclusive).
untilstringyesISO 8601 end of the export window (inclusive). Max 90-day window.
formatstringnojsonl (default) or csv.

Response

The response body is streamed with chunked-transfer encoding — no Content-Length header is set. The Content-Disposition header carries the suggested filename.

HeaderValue
Content-Typeapplication/x-ndjson (jsonl) or text/csv
Content-Dispositionattachment; filename="zeridion-jobs-{projectId}-{date}.{ext}"

JSONL format

One JSON object per line, ordered by created_at ascending, then id ascending. Each row mirrors the full job detail object — all fields including payload, error, and tags.

{"id":"job_01J...","state":"succeeded","job_type":"email.send","queue":"email","created_at":"2026-04-01T00:00:00Z","completed_at":"2026-04-01T00:00:01Z","attempt":1,"max_attempts":3,"duration_ms":240,"error":null,"tags":{"env":"prod"}}
{"id":"job_01J...","state":"failed","job_type":"report.generate","queue":"default","created_at":"2026-04-01T00:05:00Z","completed_at":"2026-04-01T00:05:30Z","attempt":3,"max_attempts":3,"duration_ms":30000,"error":{"type":"TimeoutException","message":"Worker timeout"},"tags":null}

CSV format

RFC-4180 compliant. First row is the header. Object fields (payload, error, tags) are serialized as JSON strings.

Id,State,JobType,Queue,CreatedAt,CompletedAt,Attempt,MaxAttempts,DurationMs,Error,Tags
job_01J...,succeeded,email.send,email,2026-04-01T00:00:00Z,2026-04-01T00:00:01Z,1,3,240,,"{""env"":""prod""}"

curl examples

# JSONL export — all jobs in April 2026
curl -G "https://api.zeridion.com/flare/v1/jobs/export" \
-H "Authorization: Bearer $ZERIDION_API_KEY" \
--data-urlencode "from=2026-04-01T00:00:00Z" \
--data-urlencode "until=2026-04-30T23:59:59Z" \
--data-urlencode "format=jsonl" \
-o jobs-april.jsonl

# CSV export — last 30 days
curl -G "https://api.zeridion.com/flare/v1/jobs/export" \
-H "Authorization: Bearer $ZERIDION_API_KEY" \
--data-urlencode "from=$(date -u -v-30d +%Y-%m-%dT%H:%M:%SZ)" \
--data-urlencode "until=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--data-urlencode "format=csv" \
-o jobs-last-30d.csv

Error responses

CodeHTTPDescription
invalid_from400from is missing or not a valid ISO 8601 date-time.
invalid_until400until is missing or not a valid ISO 8601 date-time.
invalid_range400from is not before until.
range_too_large400The requested window exceeds 90 days.
invalid_format400format is not "jsonl" or "csv".
rate_limit_exceeded429A previous export from this project is still inside the 60-second cooldown. Retry after Retry-After.

See also