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
| Field | Type | Required | Description |
|---|---|---|---|
job_type | string | Yes | Job type identifier, typically the fully qualified class name. Max 500 characters. |
payload | object | Yes | JSON payload passed to the job at execution time. |
queue | string | No | Target queue name. Max 100 characters. Default: "default". |
run_at | string (ISO 8601) | No | Schedule the job for future execution. Omit for immediate. |
max_attempts | integer | No | Maximum execution attempts (1–100). Default: 3. |
timeout_seconds | integer | No | Per-attempt timeout (1–86,400). Default: 1800 (30 minutes). |
idempotency_key | string | No | Unique key for deduplication. Max 200 characters. |
tags | object | No | Key-value string pairs (Dictionary<string, string>) visible in the dashboard. Both keys and values must be strings. |
parent_job_id | string | No | Create 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
}
| Field | Type | Description |
|---|---|---|
id | string | Unique job identifier |
state | string | Initial state: pending (immediate) or scheduled (has run_at or parent_job_id) |
job_type | string | Echoed job type |
queue | string | Resolved queue name |
created_at | string (ISO 8601) | Timestamp of creation |
run_at | string (ISO 8601) | Scheduled execution time, or null for immediate |
attempt | integer | Current attempt number (starts at 0) |
max_attempts | integer | Maximum attempts allowed |
Errors
| Status | Code | Condition |
|---|---|---|
| 400 | invalid_request | Validation failed (see validation rules) |
| 402 | billing_state_blocked | Project'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. |
| 409 | idempotency_key_reuse | The idempotency_key was previously used with a different request body. Same-key + same-body replays the original 201 instead. |
| 409 | parent_terminal | Parent job is cancelled or dead_letter |
| 422 | parent_not_found | parent_job_id does not reference an existing job |
Idempotent replay. When the same
idempotency_keyis sent with the same body — or when anIdempotency-Keyrequest header is reused — the server returns the original201 Createdresponse verbatim, plus anIdempotent-Replay: trueresponse header. New clients should prefer theIdempotency-Keyheader; the body-fieldidempotency_keyis 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" }
}
| Field | Type | Description |
|---|---|---|
id | string | Unique job identifier |
state | string | Current job state: pending, scheduled, processing, succeeded, failed, cancelled, dead_letter |
job_type | string | Job type identifier |
queue | string | Queue the job belongs to |
payload | object | The job's JSON payload |
created_at | string (ISO 8601) | When the job was created |
started_at | string (ISO 8601) | When execution began, or null |
completed_at | string (ISO 8601) | When execution finished, or null |
attempt | integer | Current attempt number |
max_attempts | integer | Maximum attempts allowed |
progress | number | Progress value (0.0–1.0) reported by the job, or null |
duration_ms | integer | Execution duration in milliseconds, or null |
error | object | Error details if the job failed (see below), or null |
tags | object | Key-value string pairs (Dictionary<string, string>), or null |
Error detail object
When a job fails, the error field contains:
| Field | Type | Description |
|---|---|---|
type | string | Exception type name |
message | string | Error message |
stack_trace | string | Stack trace from the failed attempt |
Errors
| Status | Code | Condition |
|---|---|---|
| 404 | job_not_found | Job 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
| Param | Type | Default | Description |
|---|---|---|---|
state | string | all | Filter by state: pending, scheduled, processing, succeeded, failed, cancelled, dead_letter |
queue | string | all | Filter by queue name |
job_type | string | all | Filter by job type |
created_after | string (ISO 8601) | — | Only return jobs created after this timestamp |
created_before | string (ISO 8601) | — | Only return jobs created before this timestamp |
limit | integer | 50 | Number of results per page (1–100) |
cursor | string | — | Cursor 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"
}
| Field | Type | Description |
|---|---|---|
data | array | Array of job detail objects |
has_more | boolean | true if more results exist beyond this page. When false, next_cursor is always null. |
next_cursor | string | Pass 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:
- Make the initial request without a
cursor - If
has_moreistrue, use thenext_cursorvalue as thecursorparameter in the next request - Repeat until
has_moreisfalse
:::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
scheduledstate.
Errors
| Status | Code | Condition |
|---|---|---|
| 403 | forbidden | Caller's project-membership role is below member (cancellation requires RoleRank >= member) |
| 404 | job_not_found | Job does not exist |
| 409 | invalid_state | Job 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
}
| Field | Type | Description |
|---|---|---|
id | string | Job identifier |
state | string | Always pending after a successful retry |
attempt | integer | Current attempt number |
Behavior
- Only jobs in
failedordead_letterstate can be retried. - If the job has exhausted all attempts (
attempt >= max_attempts), themax_attemptsis automatically bumped toattempt + 1to allow the retry. - The job's error details, timing fields, and worker assignment are cleared.
Errors
| Status | Code | Condition |
|---|---|---|
| 404 | job_not_found | Job does not exist |
| 409 | invalid_state | Job 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 state | Child initial state | Behavior |
|---|---|---|
pending / scheduled / processing | scheduled | Child waits for parent to complete |
succeeded | pending | Child runs immediately (parent already done) |
cancelled / dead_letter | — | Creation rejected with 409 parent_terminal |
Activation and cascading
- Parent succeeds: all child jobs in
scheduledstate are activated (moved topending). The parent's completion ack response includeschildren_activatedwith the count. - Parent dead-letters: all child jobs in
scheduledstate are cancelled. - Parent is cancelled: all child jobs in
scheduledstate are cancelled.
Errors
| Status | Code | Condition |
|---|---|---|
| 409 | parent_terminal | Parent is cancelled or dead_letter |
| 422 | parent_not_found | The 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
| Field | Type | Description |
|---|---|---|
state | string | Current job state (pending, processing, succeeded, failed, cancelled, dead_letter) |
progress | number | Progress in [0.0, 1.0] reported by the job, or null |
attempt | integer | Current attempt number |
max_attempts | integer | Maximum attempts allowed |
Connection lifecycle
- First tick: the server validates the job exists (returns 404 if not) and immediately emits the current snapshot without waiting for a change.
- Subsequent ticks: a new snapshot is emitted only when
stateorprogressdiffers from the previous tick, keeping idle connections cheap. - Terminal states: the stream closes automatically when
statereachessucceeded,failed,cancelled, ordead_letter. - 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
| Status | Code | Condition |
|---|---|---|
| 404 | job_not_found | Job 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_exceededwithRetry-After. The export window is also capped at 90 days — requests with a wider span return400 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
| Parameter | Type | Required | Description |
|---|---|---|---|
from | string | yes | ISO 8601 start of the export window (inclusive). |
until | string | yes | ISO 8601 end of the export window (inclusive). Max 90-day window. |
format | string | no | jsonl (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.
| Header | Value |
|---|---|
Content-Type | application/x-ndjson (jsonl) or text/csv |
Content-Disposition | attachment; 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
| Code | HTTP | Description |
|---|---|---|
invalid_from | 400 | from is missing or not a valid ISO 8601 date-time. |
invalid_until | 400 | until is missing or not a valid ISO 8601 date-time. |
invalid_range | 400 | from is not before until. |
range_too_large | 400 | The requested window exceeds 90 days. |
invalid_format | 400 | format is not "jsonl" or "csv". |
rate_limit_exceeded | 429 | A previous export from this project is still inside the 60-second cooldown. Retry after Retry-After. |
See also
- Recurring Jobs API — cron-driven schedules that auto-enqueue jobs
- Idempotency guide — safe retries with
Idempotency-Key - Job Continuations guide — chain dependent jobs on success or failure
- Errors —
job_not_found,quota_exceeded,idempotency_key_reuse, and the rest