Scheduled jobs

Dispatches jobs to pull workers or signed HTTPS callbacks, with schedules, bounded retries, and dead-letter replay. $2 per 100,000 jobs, retries, or operations + $0.001 per schedule-day. The base URL is https://api.motor.dev/scheduled_jobs, and the OpenAPI 3.1 specification describes each endpoint.

Overview

Run work later, retry failed attempts, and control how quickly work reaches a provider. Use recurring schedules for repeat tasks and individual jobs for one-time work.

How it works

A queue sets the delivery method, concurrency, and rate limits for its jobs. Motor can send each job to an HTTPS handler, or a worker can claim it with a lease. A recurring schedule creates jobs in a queue at the times you specify.

Execution and retries

Jobs can be delivered more than once. Verify callback signatures and deduplicate effects by job ID. HTTP callbacks run for at most 30 seconds; use pull workers for longer work. Delays and deadlines are limited to 7 days from submission; set deadline_at explicitly for delays beyond 24 hours.

Payloads are limited to 64 KiB and retained for 7 days after completion unless deleted sooner. Use a separate queue and staging worker for testing; Scheduled jobs does not have a test mode.

Pricing

Scheduled jobs doesn't include a free allowance.

MeterPriceCharged for
queues.dispatch$2.00 / 100,000Each accepted job, its first attempt included, and each further claimed attempt
queues.operation$2.00 / 100,000Each read or list page, pull lease request (including empty polls), heartbeat, job rebind, and queue or schedule creation, update, rotation, or rebind
queues.schedule_day$0.001Each started 24-hour period of a schedule that you keep, active or paused, paid in advance

An accepted job costs one unit whether it succeeds, fails, is canceled, or expires. Validation, cancellation, deletion, acknowledgments, job resumes, and idempotent or deduplicated request replays don't incur an additional charge. Resuming a schedule is a paid update and may also start a new prepaid schedule-day. A refused request costs nothing. Accepted work isn't refunded, and a begun schedule-day isn't either.

ExampleCharge
Dispatch for a job that succeeds on its first attempt$0.00002
Admission of a job you cancel before its first attempt$0.00002
Dispatch for a job that succeeds after three retries$0.00008
Admission of ten new jobs in a batch$0.00020
A daily schedule kept for 30 days: creation and 30 jobs, each succeeding on its first attempt$0.03062
Ten status reads$0.00020

A job's max_cost_usd caps its dispatch (its attempts at $0.00002 each), and a schedule's max_total_cost_usd caps its dispatch and schedule-days. Neither covers operations. Set a monthly spending limit on each API key to cap that key's usage. Requests billed as operations return Motor-Operation-Cost-USD. Jobs keep the rates that they were accepted at, and schedules keep theirs until they end. Operations are charged at the rate of the time they happen.

Limits and retention

Resource or operationLimit
Queues100
Schedules100
Unfinished jobs, leased and delayed included10,000
Stored payloads, results, and templates64 MiB
Decoded payload of a job64 KiB
Jobs of a batch10, 640 KiB in all
Delay and deadline of a job7 days
Attempts per job10 maximum; 4 or fewer by default
Active FIFO groups of a queue1,000
Reads and configuration20 a second, bursts of 40
Job submissions100 a second
Pull lease requests20 a second
Heartbeats and acknowledgments200 a second, bursts of 400

Your team's limits are in each queue's limits; ask support@motor.dev to raise them. Reaching a capacity limit causes new requests to fail with 409 capacity_exceeded and the limit it reached. Accepted jobs remain available until they finish or expire, subject to deletion. Payloads and results are retained for 7 days after a job finishes; its job and attempt records remain for 35 days after completion. A deleted job's contents go at once. An empty queue that nothing used for 30 days is deleted, after a queues.queue.expiring event 7 days before. Backups follow Motor's backup lifecycle and can't be erased selectively at once.

Examples

Enrich accounts at a controlled rate

Enrich accounts through a provider that allows 5 requests per second. Deploy an HTTPS handler that verifies Motor’s signature, deduplicates by job ID, and makes one provider request per attempt. The queue limits calls to your handler; coordinate other traffic to the same provider separately.

Use your secret key as MOTOR_KEY. Examples show trimmed responses; replace sample IDs and secrets with values returned by Motor. Save each Idempotency-Key before sending and reuse it if the response is lost.

curl -i "https://api.motor.dev/scheduled_jobs/queues" \
  -H "Authorization: Bearer $MOTOR_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"name": "Account enrichment", "delivery": "http", "destination_url": "https://worker.example.com/enrich", "rate_per_second": 5}'
HTTP 201

{
  "id": "que_...",
  "delivery": "http",
  "rate_per_second": 5,
  "signing_secret": "whsec_...",
  "version": "1"
}

Store the signing secret securely. The queue allows five attempts per second, with bursts of up to five; add worker-side pacing if your provider enforces a strict sliding window. Submit one job per account; this payload is {"account_id":"acct_42"}.

curl -i "https://api.motor.dev/scheduled_jobs/jobs" \
  -H "Authorization: Bearer $MOTOR_KEY" \
  -H "Idempotency-Key: enrich-acct_42-v1" \
  -H "Content-Type: application/json" \
  -d '{"queue_id": "que_...", "payload_base64": "eyJhY2NvdW50X2lkIjoiYWNjdF80MiJ9", "deduplication_key": "enrich-acct_42-v1"}'
HTTP 202

{
  "id": "qjb_...",
  "state": "queued",
  "attempt_count": 0,
  "max_attempts": 4,
  "deduplicated": false
}

If the provider answers 429 with Retry-After, forward that status and header from your handler. Motor waits at least that long (up to a day) before retrying, subject to the job’s attempts, budget, and deadline. Return 2xx only after recording success durably. Poll the job to see the result:

curl -i "https://api.motor.dev/scheduled_jobs/jobs/qjb_..." \
  -H "Authorization: Bearer $MOTOR_KEY"
HTTP 200

{
  "id": "qjb_...",
  "state": "succeeded",
  "attempt_count": 1,
  "last_outcome": "succeeded"
}

Scheduled jobs is live-only. For a full workflow, see receive a Stripe event and enqueue work.

Receive a Stripe event and schedule its work

Use a Webhooks mailbox to receive Stripe events, then submit each event to a queue that limits worker calls to five per second. Complete the Stripe mailbox example first. Use its source ID below.

curl -i "https://api.motor.dev/scheduled_jobs/queues" \
  -H "Authorization: Bearer $MOTOR_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"name": "Stripe processing", "delivery": "http", "destination_url": "https://worker.example.com/stripe", "rate_per_second": 5}'
HTTP 201

{
  "id": "que_...",
  "rate_per_second": 5,
  "signing_secret": "whsec_..."
}

Configure your worker to verify the queue signature and deduplicate by job ID. Choose a recent polling window:

curl -i "https://api.motor.dev/webhooks/messages?source_id=wds_...&created_after=2026-11-01T00:00:00Z" \
  -H "Authorization: Bearer $MOTOR_KEY"
HTTP 200

{
  "data": [
    {
      "id": "wdm_...",
      "status": "no_destinations",
      "content_available": true
    }
  ],
  "next_cursor": null
}
curl -i "https://api.motor.dev/webhooks/messages/wdm_.../content" \
  -H "Authorization: Bearer $MOTOR_KEY"
HTTP 200

{
  "id": "evt_...",
  "type": "invoice.paid",
  "data": {
    "object": {
      "id": "in_..."
    }
  }
}

Validate the event type, then enqueue a JSON object containing the Motor message ID. The worker can read the retained body with its own Motor key; do not copy a body larger than the queue’s 64 KiB limit into the job.

curl -i "https://api.motor.dev/scheduled_jobs/jobs" \
  -H "Authorization: Bearer $MOTOR_KEY" \
  -H "Idempotency-Key: stripe-wdm_..." \
  -H "Content-Type: application/json" \
  -d '{"queue_id": "que_...", "payload_base64": "eyJtZXNzYWdlX2lkIjoid2RtXy4uLiJ9", "deduplication_key": "stripe-wdm_..."}'
HTTP 202

{
  "id": "qjb_...",
  "state": "queued",
  "deduplicated": false
}

The example payload decodes to a message_id of wdm_...; generate it from the real ID. Save how far you’ve read through the mailbox only after the job is accepted. Follow every next_cursor and reuse the same deduplication_key on a retry. Read the body before its 30-day retention expires; queue deadlines are at most 7 days.

Follow up in 3 days unless canceled

Schedule a follow-up and cancel it if the customer responds. Replace both timestamps with future UTC times, 3 and 4 days from submission; the default 24-hour deadline is too short.

curl -i "https://api.motor.dev/scheduled_jobs/jobs" \
  -H "Authorization: Bearer $MOTOR_KEY" \
  -H "Idempotency-Key: follow-up-customer-42" \
  -H "Content-Type: application/json" \
  -d '{"queue_id": "que_...", "payload_base64": "e30=", "not_before": "2026-11-04T12:00:00Z", "deadline_at": "2026-11-05T12:00:00Z", "deduplication_key": "follow-up-customer-42"}'
HTTP 202

{
  "id": "qjb_...",
  "state": "scheduled",
  "version": "1"
}

Read the current job and use its ETag to cancel. This example assumes it is still waiting:

curl -i "https://api.motor.dev/scheduled_jobs/jobs/qjb_..." \
  -H "Authorization: Bearer $MOTOR_KEY"
HTTP 200
ETag: "queues:qjb_...:1"

{
  "id": "qjb_...",
  "state": "scheduled",
  "version": "1"
}
curl -i -X POST "https://api.motor.dev/scheduled_jobs/jobs/qjb_.../cancel" \
  -H "Authorization: Bearer $MOTOR_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H 'If-Match: "queues:qjb_...:1"'
HTTP 200

{
  "id": "qjb_...",
  "state": "canceled",
  "cancel_requested": true
}

Cancellation cannot undo an attempt already in progress; check the returned state.

API reference

Try it

Sign in to try the API here with one of your team's keys. Requests use that key's permissions and spending limit. Standard charges apply.

Queues

Verifying callbacks

Motor signs every attempt with the Standard Webhooks scheme, so your endpoint can use its maintained libraries. Each request has:

  • webhook-id: the job ID. It's the same on every retry of the job: deduplicate on it.
  • webhook-timestamp: Unix seconds of this attempt.
  • webhook-signature: v1, and the Base64 HMAC-SHA256 of the ID, ., the timestamp, ., and the exact body, with the Base64-decoded signing key after removing whsec_. For 24 hours after a rotation there are two signatures, separated by a space.
  • Motor-Attempt-Id, Motor-Attempt-Number, Motor-Queue-Id, and, for a schedule's job, Motor-Scheduled-At: for diagnostics. The signature doesn't cover them.
from standardwebhooks import Webhook

wh = Webhook("whsec_...")
wh.verify(request.body, request.headers)  # raises if invalid
job_id = request.headers["webhook-id"]
if already_done(job_id):
    return 200  # a retry of work you finished
do_work_and_record(job_id, request.body)  # one transaction
return 200

Compare signatures in constant time, reject timestamps more than 5 minutes from now, and record the job ID in the same transaction as your side effect. Answer any 2xx once the work is durably accepted, and do long work after you answer: Motor waits at most the queue's timeout_seconds (at most 30 seconds). Treat the payload as untrusted data: a valid signature authenticates the delivery, not the safety of its contents.

The queue's authorization and custom headers go with each request. Destinations are HTTPS on port 443 to a public host, without a query string: put credentials in authorization and verify the signed body. Motor doesn't follow redirects, sends over IPv4, and doesn't have static sender addresses. Rotate the key with POST /queues/{queue_id}/rotate_secret and If-Match.

Ordering and limits

An unordered queue starts eligible jobs by due time, then submission order. Jobs may finish in a different order. A concurrency of 1 doesn't make it strictly ordered.

A fifo queue needs a group_id on each job. Only the oldest unfinished job in a group can start. Its delays and retries block later jobs in that group; other groups can continue. A dead, canceled, or expired job releases its group, and a replay joins the end of it. Only the order of claims is guaranteed: a receiver whose lease ended may still be working, so your effects need their own idempotency.

A queue's concurrency (1 to 100, 10 by default) and rate_per_second (1 to 100, 10 by default) limit its attempts. Your team's attempts in flight and per second have limits of 100 too. Pausing a queue or lowering its limits affects later claims; it never recalls a request that Motor sent. A job that waits shows why in waiting_reason, such as fifo_blocked or queue_paused. A job that waits for a concurrency slot has a due_at of null. It starts when an attempt ends and frees a slot. The queues of your team take turns for your team's slots.

deduplication_key makes a submission idempotent for 7 days within its queue: the same key and input return the original job with deduplicated: true and no charge, and a different input gets 409 deduplication_conflict. POST /jobs/batch submits up to 10 jobs, to one or several queues, all or nothing.

Keep each customer’s jobs in order

Use a FIFO queue with one group per customer. Workers claim each customer’s jobs in order; other customers’ jobs can run in parallel.

curl -i "https://api.motor.dev/scheduled_jobs/queues" \
  -H "Authorization: Bearer $MOTOR_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"name": "Customer updates", "delivery": "pull", "ordering": "fifo"}'
HTTP 201

{
  "id": "que_fifo",
  "ordering": "fifo",
  "delivery": "pull"
}
curl -i "https://api.motor.dev/scheduled_jobs/jobs" \
  -H "Authorization: Bearer $MOTOR_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"queue_id": "que_fifo", "payload_base64": "e30=", "group_id": "customer-42"}'
HTTP 202

{
  "id": "qjb_...",
  "group_id": "customer-42",
  "group_sequence": "1",
  "state": "queued"
}

Use the same group_id on later jobs for this customer. Retries and delays hold the group; a dead, canceled, or expired job releases it. Jobs start in order, but a worker may keep running after its lease expires. Prevent repeated jobs from applying the same change twice.

POST/scheduled_jobs/queues

Create a queue

Creates an http queue, which Motor delivers as signed HTTPS POSTs to destination_url, or a pull queue, which your workers lease. The answer to an HTTP queue's creation shows its signing_secret once; an identical retry shows it again only to the same key while the queue keeps that key. Costs one operation. At most 100 queues by default.

ParameterLocationDescription
Idempotency-Keyheader1 to 128 printable ASCII characters that you choose for this operation. An identical retry returns the first response with Idempotency-Replayed: true for 35 days, without a new charge; a different request with the key is 409 idempotency_conflict, and a retry while the first runs is 409 request_in_progress. A missing key is 400 idempotency_key_required.
Motor-ModeheaderScheduled jobs is live-only. Omit this header or send live; test gives 422 unsupported_mode before anything else happens.
Request body
{
  "name": "Invoices",
  "delivery": "http",
  "destination_url": "https://agent.example.com/jobs",
  "headers": {
    "X-Team": "billing"
  },
  "authorization": "Bearer agent-token"
}
FieldTypeDescription
namerequiredstring
deliveryrequiredstringImmutable.
orderingstringImmutable.
concurrencyinteger
rate_per_secondinteger
pausedboolean
metadataobjectAt most 16 string pairs: keys of 1 to 64 bytes, values of at most 256, at most 2 KiB in all. A job's run_id value is filterable.
destination_urlstringHTTP queues only, and required for them: HTTPS on port 443 to a public host, without user information, a query, or a fragment.
headersobjectAt most 16 custom headers, 4 KiB in all. Motor's own headers, hop-by-hop headers, Authorization, Cookie, Proxy-Authorization, and webhook-*, motor-*, and x-forwarded-* names aren't allowed.
authorizationstring or nullHTTP queues only: the write-only Authorization value of each request. null clears it.
timeout_secondsinteger
retry_delays_secondsarray
Responses

201The queue, with its signing key if it's an HTTP queue.

{
  "id": "que_8Fq2RkV7xYz3LmN4pQs9Td",
  "object": "queue",
  "name": "Invoices",
  "delivery": "http",
  "ordering": "unordered",
  "concurrency": 10,
  "rate_per_second": 10,
  "paused": false,
  "pause_reason": null,
  "metadata": {},
  "destination": "https://agent.example.com/jobs",
  "destination_revision": "1",
  "headers": {
    "x-team": "billing"
  },
  "has_authorization": true,
  "timeout_seconds": 10,
  "retry_delays_seconds": [
    10,
    60,
    300
  ],
  "signing_secret_version": 1,
  "previous_secret_expires_at": null,
  "limits": {
    "max_attempts": 4,
    "default_attempts": 4,
    "max_payload_bytes": 65536,
    "max_delivery_horizon_seconds": 604800,
    "team_concurrency": 100,
    "team_dispatch_rate": 100,
    "team_pending_jobs": 10000
  },
  "counts": {
    "depth": 0,
    "leased": 0,
    "oldest_due_age_seconds": 0,
    "counted_at": "2026-10-17T09:00:00.000Z"
  },
  "version": "1",
  "created_at": "2026-10-17T09:00:00.000Z",
  "updated_at": "2026-10-17T09:00:00.000Z",
  "signing_secret": "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"
}

GET/scheduled_jobs/queues

List queues

Lists your team's queues, oldest first. Filters: delivery and paused. Costs one operation per page.

ParameterLocationDescription
limitqueryItems per page: 1 to 100, 50 by default. A page also stops before 128 KiB.
cursorqueryThe next_cursor of the previous page. It's bound to your team, the route, and its filters, works for an hour, and doesn't see items created after the first page.
deliveryquery
pausedquery
Motor-ModeheaderScheduled jobs is live-only. Omit this header or send live; test gives 422 unsupported_mode before anything else happens.
Responses

200A page of queues.

{
  "items": [
    {
      "id": "que_8Fq2RkV7xYz3LmN4pQs9Td",
      "object": "queue",
      "name": "Invoices",
      "delivery": "http",
      "ordering": "unordered",
      "concurrency": 10,
      "rate_per_second": 10,
      "paused": false,
      "pause_reason": null,
      "metadata": {},
      "destination": "https://agent.example.com/jobs",
      "destination_revision": "1",
      "headers": {
        "x-team": "billing"
      },
      "has_authorization": true,
      "timeout_seconds": 10,
      "retry_delays_seconds": [
        10,
        60,
        300
      ],
      "signing_secret_version": 1,
      "previous_secret_expires_at": null,
      "limits": {
        "max_attempts": 4,
        "default_attempts": 4,
        "max_payload_bytes": 65536,
        "max_delivery_horizon_seconds": 604800,
        "team_concurrency": 100,
        "team_dispatch_rate": 100,
        "team_pending_jobs": 10000
      },
      "counts": {
        "depth": 0,
        "leased": 0,
        "oldest_due_age_seconds": 0,
        "counted_at": "2026-10-17T09:00:00.000Z"
      },
      "version": "1",
      "created_at": "2026-10-17T09:00:00.000Z",
      "updated_at": "2026-10-17T09:00:00.000Z"
    }
  ],
  "next_cursor": null
}

GET/scheduled_jobs/queues/{queue_id}

Get a queue

Returns a queue with its effective limits and its counts. Costs one operation.

ParameterLocationDescription
queue_idpathque_...
Motor-ModeheaderScheduled jobs is live-only. Omit this header or send live; test gives 422 unsupported_mode before anything else happens.
Responses

200The queue.

{
  "id": "que_8Fq2RkV7xYz3LmN4pQs9Td",
  "object": "queue",
  "name": "Invoices",
  "delivery": "http",
  "ordering": "unordered",
  "concurrency": 10,
  "rate_per_second": 10,
  "paused": false,
  "pause_reason": null,
  "metadata": {},
  "destination": "https://agent.example.com/jobs",
  "destination_revision": "1",
  "headers": {
    "x-team": "billing"
  },
  "has_authorization": true,
  "timeout_seconds": 10,
  "retry_delays_seconds": [
    10,
    60,
    300
  ],
  "signing_secret_version": 1,
  "previous_secret_expires_at": null,
  "limits": {
    "max_attempts": 4,
    "default_attempts": 4,
    "max_payload_bytes": 65536,
    "max_delivery_horizon_seconds": 604800,
    "team_concurrency": 100,
    "team_dispatch_rate": 100,
    "team_pending_jobs": 10000
  },
  "counts": {
    "depth": 0,
    "leased": 0,
    "oldest_due_age_seconds": 0,
    "counted_at": "2026-10-17T09:00:00.000Z"
  },
  "version": "1",
  "created_at": "2026-10-17T09:00:00.000Z",
  "updated_at": "2026-10-17T09:00:00.000Z"
}

PATCH/scheduled_jobs/queues/{queue_id}

Update a queue

Changes the fields that you send; the others stay. delivery and ordering can't change, and authorization: null clears the authorization. A change of the destination, its headers, or its authorization adds one to destination_revision and reaches waiting jobs at their next claim. A new host needs its own authorization and headers in the same request. paused: false resumes a queue that Motor paused. Costs one operation.

ParameterLocationDescription
queue_idpathque_...
If-MatchheaderThe resource's ETag, "queues:{id}:{version}". A missing header is 428 precondition_required, and a stale one 412 precondition_failed.
Motor-ModeheaderScheduled jobs is live-only. Omit this header or send live; test gives 422 unsupported_mode before anything else happens.
Request body
{
  "paused": false,
  "destination_url": "https://agent.example.com/v2/jobs"
}
FieldTypeDescription
namestring
concurrencyinteger
rate_per_secondinteger
pausedboolean
metadataobjectAt most 16 string pairs: keys of 1 to 64 bytes, values of at most 256, at most 2 KiB in all. A job's run_id value is filterable.
destination_urlstringHTTP queues only, and required for them: HTTPS on port 443 to a public host, without user information, a query, or a fragment.
headersobjectAt most 16 custom headers, 4 KiB in all. Motor's own headers, hop-by-hop headers, Authorization, Cookie, Proxy-Authorization, and webhook-*, motor-*, and x-forwarded-* names aren't allowed.
authorizationstring or nullHTTP queues only: the write-only Authorization value of each request. null clears it.
timeout_secondsinteger
retry_delays_secondsarray
Responses

200The queue.

{
  "id": "que_8Fq2RkV7xYz3LmN4pQs9Td",
  "object": "queue",
  "name": "Invoices",
  "delivery": "http",
  "ordering": "unordered",
  "concurrency": 10,
  "rate_per_second": 10,
  "paused": false,
  "pause_reason": null,
  "metadata": {},
  "destination": "https://agent.example.com/jobs",
  "destination_revision": "1",
  "headers": {
    "x-team": "billing"
  },
  "has_authorization": true,
  "timeout_seconds": 10,
  "retry_delays_seconds": [
    10,
    60,
    300
  ],
  "signing_secret_version": 1,
  "previous_secret_expires_at": null,
  "limits": {
    "max_attempts": 4,
    "default_attempts": 4,
    "max_payload_bytes": 65536,
    "max_delivery_horizon_seconds": 604800,
    "team_concurrency": 100,
    "team_dispatch_rate": 100,
    "team_pending_jobs": 10000
  },
  "counts": {
    "depth": 0,
    "leased": 0,
    "oldest_due_age_seconds": 0,
    "counted_at": "2026-10-17T09:00:00.000Z"
  },
  "version": "1",
  "created_at": "2026-10-17T09:00:00.000Z",
  "updated_at": "2026-10-17T09:00:00.000Z"
}

DELETE/scheduled_jobs/queues/{queue_id}

Delete a queue

Marks the queue as deleted immediately: it rejects new jobs, stops starting attempts, revokes its credentials, and stops its schedules. A cleanup then cancels its pending jobs and erases their contents. A request already sent can't be recalled. No additional charge.

ParameterLocationDescription
queue_idpathque_...
If-MatchheaderThe resource's ETag, "queues:{id}:{version}". A missing header is 428 precondition_required, and a stale one 412 precondition_failed.
Motor-ModeheaderScheduled jobs is live-only. Omit this header or send live; test gives 422 unsupported_mode before anything else happens.
Responses

202The deletion receipt.

{
  "id": "que_8Fq2RkV7xYz3LmN4pQs9Td",
  "object": "queue_deletion",
  "deleted": true,
  "deleted_at": "2026-10-17T09:00:00.000Z",
  "cleanup": "pending"
}

POST/scheduled_jobs/queues/{queue_id}/rotate_secret

Rotate a signing key

Makes a new signing key for an HTTP queue and shows it once. The previous key keeps signing alongside it for 24 hours; another rotation in that time is 409 invalid_state. Costs one operation.

ParameterLocationDescription
queue_idpathque_...
If-MatchheaderThe resource's ETag, "queues:{id}:{version}". A missing header is 428 precondition_required, and a stale one 412 precondition_failed.
Idempotency-Keyheader1 to 128 printable ASCII characters that you choose for this operation. An identical retry returns the first response with Idempotency-Replayed: true for 35 days, without a new charge; a different request with the key is 409 idempotency_conflict, and a retry while the first runs is 409 request_in_progress. A missing key is 400 idempotency_key_required.
Motor-ModeheaderScheduled jobs is live-only. Omit this header or send live; test gives 422 unsupported_mode before anything else happens.
Request body
{}
Responses

200The queue with its new key.

{
  "id": "que_8Fq2RkV7xYz3LmN4pQs9Td",
  "object": "queue",
  "name": "Invoices",
  "delivery": "http",
  "ordering": "unordered",
  "concurrency": 10,
  "rate_per_second": 10,
  "paused": false,
  "pause_reason": null,
  "metadata": {},
  "destination": "https://agent.example.com/jobs",
  "destination_revision": "1",
  "headers": {
    "x-team": "billing"
  },
  "has_authorization": true,
  "timeout_seconds": 10,
  "retry_delays_seconds": [
    10,
    60,
    300
  ],
  "signing_secret_version": 1,
  "previous_secret_expires_at": null,
  "limits": {
    "max_attempts": 4,
    "default_attempts": 4,
    "max_payload_bytes": 65536,
    "max_delivery_horizon_seconds": 604800,
    "team_concurrency": 100,
    "team_dispatch_rate": 100,
    "team_pending_jobs": 10000
  },
  "counts": {
    "depth": 0,
    "leased": 0,
    "oldest_due_age_seconds": 0,
    "counted_at": "2026-10-17T09:00:00.000Z"
  },
  "version": "1",
  "created_at": "2026-10-17T09:00:00.000Z",
  "updated_at": "2026-10-17T09:00:00.000Z",
  "signing_secret": "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"
}

Jobs

Retries, deadlines, and unknown outcomes

Jobs can be delivered more than once. When Motor can't know an attempt's outcome, such as when your endpoint timed out or a worker's lease ended, the attempt is unknown and the job retries under its policy. Your endpoint or worker may then see the job again, so make every effect idempotent by the job ID.

A queue's retry_delays_seconds (10, 60, and 300 by default; up to 9 delays) sets the waits between attempts, plus 0 to 20% of jitter. A job makes at most max_attempts attempts, including the first: 4 by default, or fewer if the queue allows fewer retries. A 429 or 503 with Retry-After waits at least that long, up to a day.

  • Any 2xx succeeds.
  • 408, 425, 429, 5xx, timeouts, and network errors retry.
  • Redirects and other 4xx answers fail the job without a retry.
  • 410 and an unsafe destination fail the job and pause the queue with a pause_reason. Fix the queue, then update it with paused: false.

A job's deadline_at (24 hours after submission by default, at most 7 days) stops new attempts: a job whose next attempt can't start in time is expired. An attempt that is under way when the deadline passes can't be undone.

POST /jobs/{job_id}/cancel with If-Match ends a waiting job at once. If an attempt is already running, it can finish or run until its lease expires. Motor won’t retry it, but it may still complete the work. If the attempt succeeds, the job is succeeded with cancel_requested: true. Canceling a finished job changes nothing.

# Cancel, then check whether the effect may have happened.
curl -X POST https://api.motor.dev/scheduled_jobs/jobs/qjb_.../cancel \
  -H "Authorization: Bearer $MOTOR_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H 'If-Match: "queues:qjb_...:3"'
curl https://api.motor.dev/scheduled_jobs/jobs/qjb_.../attempts \
  -H "Authorization: Bearer $MOTOR_KEY"  # an outcome of unknown means it may have

Dead jobs, blocked jobs, and replay

A job is blocked, with its deadline still running, when its key can't pay a retry (billing_limit), its key was revoked or lost access to scheduled jobs (key_revoked), or your team is suspended. Fix the cause, then resume the job at no additional charge:

curl -X POST https://api.motor.dev/scheduled_jobs/jobs/qjb_.../resume \
  -H "Authorization: Bearer $MOTOR_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H 'If-Match: "queues:qjb_...:5"'

POST /jobs/{job_id}/rebind, sent with the key that should pay, uses that key’s permissions and spending limit for future attempts. It also resumes a job blocked by a key or billing problem. Schedules have their own rebind; a blocked schedule resumes with any update, which starts a new prepaid day if its paid time ended.

Find failures with GET /jobs?state=dead and inspect GET /jobs/{job_id}/attempts. POST /jobs/{job_id}/replay with If-Match makes a new paid job from a dead, expired, or canceled job's retained payload, with a new ID and replay_of. If an attempt's outcome was unknown, the replay needs allow_duplicate_effects: true, because the first effect may have happened. A replay has a new job ID, so deduplicate its business action with a stable operation ID if needed:

curl -X POST https://api.motor.dev/scheduled_jobs/jobs/qjb_.../replay \
  -H "Authorization: Bearer $MOTOR_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H 'If-Match: "queues:qjb_...:6"' \
  -d '{"allow_duplicate_effects": true}'

A succeeded job can't replay: submit a new job. DELETE /jobs/{job_id} cancels a job and erases its payload and result at once; a short record stays for 35 days.

The internal Scheduled jobs API lets other Motor services include background dispatch in their own prices. Internal jobs don't appear in your queue lists; bundled dispatch doesn't add a separate queue charge.

Events

Motor sends queues.job.succeeded, .dead, .expired, .canceled, and .blocked, queues.schedule.blocked, .completed, and .expiring, and queues.queue.expiring to your event subscriptions, once for each change. Subscribe with any secret key that may use scheduled jobs:

curl https://api.motor.dev/meta/event_subscriptions \
  -H "Authorization: Bearer $MOTOR_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"service": "queues", "url": "https://agent.example.com/motor-events", "events": ["queues.job.dead", "queues.schedule.blocked"]}'

Events carry IDs, states, reasons, and costs, never payloads or results. They can repeat or arrive late: deduplicate by event ID and fetch the job’s current state before acting. There are no events for single attempts; read a job's attempts instead.

POST/scheduled_jobs/jobs

Submit a job

Accepts a job and charges its admission, which includes its first attempt: $0.00002. Answers 202 once the job, its charge, and its wakeup are stored. A matching deduplication_key answers 200 with the original job, deduplicated: true, and no charge; a different input with it is 409 deduplication_conflict. A reached capacity limit returns 409 capacity_exceeded. Accepted jobs remain available until they finish or expire, subject to deletion.

ParameterLocationDescription
Idempotency-Keyheader1 to 128 printable ASCII characters that you choose for this operation. An identical retry returns the first response with Idempotency-Replayed: true for 35 days, without a new charge; a different request with the key is 409 idempotency_conflict, and a retry while the first runs is 409 request_in_progress. A missing key is 400 idempotency_key_required.
Motor-ModeheaderScheduled jobs is live-only. Omit this header or send live; test gives 422 unsupported_mode before anything else happens.
Request body
{
  "queue_id": "que_8Fq2RkV7xYz3LmN4pQs9Td",
  "payload_base64": "eyJpbnZvaWNlIjogImludl80MiJ9",
  "not_before": "2026-10-17T09:00:00.000Z",
  "metadata": {
    "run_id": "run-7"
  }
}
FieldTypeDescription
queue_idrequiredstring
payload_base64requiredstringStandard Base64 of at most 64 KiB of bytes. Motor sends these exact bytes.
content_typestringapplication/json (the payload must be valid JSON), text/plain (UTF-8), or application/octet-stream; text types may add ; charset=utf-8.
not_beforeWhen the job may start: now by default, at most 7 days away, never in the past.
deadline_atWhen new attempts stop: 24 hours from now by default, after not_before, at most 7 days away.
max_attemptsintegerThe attempts, the first included: at most one more than the queue's retry delays, 4 or fewer by default.
max_cost_usdThe maximum dispatch cost: max_attempts × $0.00002 by default. It must cover the admission; a smaller budget lowers affordable_attempts.
group_idstringRequired on fifo queues, refused on unordered ones.
deduplication_keystringReturns the original job for the same input in this queue for 7 days.
metadataobjectAt most 16 string pairs: keys of 1 to 64 bytes, values of at most 256, at most 2 KiB in all. A job's run_id value is filterable.
Responses

202The accepted job.

{
  "id": "qjb_3VxQ7mK2pR9sLtY4nB6cWd",
  "object": "job",
  "queue_id": "que_8Fq2RkV7xYz3LmN4pQs9Td",
  "state": "queued",
  "reason": null,
  "waiting_reason": null,
  "version": "1",
  "not_before": "2026-10-17T09:00:00.000Z",
  "due_at": "2026-10-17T09:00:00.000Z",
  "deadline_at": "2026-10-18T09:00:00.000Z",
  "attempt_count": 0,
  "max_attempts": 4,
  "affordable_attempts": 4,
  "retry_delays_seconds": [
    10,
    60,
    300
  ],
  "timeout_seconds": 10,
  "lease_until": null,
  "content_type": "application/json",
  "payload_bytes": 21,
  "payload_sha256": "1d5f8c0a6a3c9b3c4e4d2f6c8b1a7e9d0c3b5a7f9e1d3c5b7a9f1e3d5c7b9a1e",
  "payload_retained": true,
  "payload_expires_at": "2026-10-18T09:00:00.000Z",
  "group_id": null,
  "group_sequence": null,
  "deduplication_key": null,
  "deduplication_expires_at": null,
  "metadata": {
    "run_id": "run-7"
  },
  "submitted_by": {
    "key_id": "key_BWBeN28Vb7cMEx7Ym8AUzs",
    "principal_id": null
  },
  "billing_key_id": "key_BWBeN28Vb7cMEx7Ym8AUzs",
  "attribution": "direct",
  "replay_of": null,
  "schedule_id": null,
  "scheduled_at": null,
  "cancel_requested": false,
  "last_outcome": null,
  "last_error_code": null,
  "retryable": null,
  "destination_revision": null,
  "cost": {
    "unit_usd": "0.00002",
    "charged_usd": "0.00002",
    "max_cost_usd": "0.00008",
    "remaining_max_cost_usd": "0.00006",
    "rate_version": "2026-10-20"
  },
  "deleted": false,
  "deleted_at": null,
  "terminal_at": null,
  "created_at": "2026-10-17T09:00:00.000Z",
  "updated_at": "2026-10-17T09:00:00.000Z",
  "deduplicated": false
}

200The original job of the deduplication key.

{
  "id": "qjb_3VxQ7mK2pR9sLtY4nB6cWd",
  "object": "job",
  "queue_id": "que_8Fq2RkV7xYz3LmN4pQs9Td",
  "state": "queued",
  "reason": null,
  "waiting_reason": null,
  "version": "1",
  "not_before": "2026-10-17T09:00:00.000Z",
  "due_at": "2026-10-17T09:00:00.000Z",
  "deadline_at": "2026-10-18T09:00:00.000Z",
  "attempt_count": 0,
  "max_attempts": 4,
  "affordable_attempts": 4,
  "retry_delays_seconds": [
    10,
    60,
    300
  ],
  "timeout_seconds": 10,
  "lease_until": null,
  "content_type": "application/json",
  "payload_bytes": 21,
  "payload_sha256": "1d5f8c0a6a3c9b3c4e4d2f6c8b1a7e9d0c3b5a7f9e1d3c5b7a9f1e3d5c7b9a1e",
  "payload_retained": true,
  "payload_expires_at": "2026-10-18T09:00:00.000Z",
  "group_id": null,
  "group_sequence": null,
  "deduplication_key": null,
  "deduplication_expires_at": null,
  "metadata": {
    "run_id": "run-7"
  },
  "submitted_by": {
    "key_id": "key_BWBeN28Vb7cMEx7Ym8AUzs",
    "principal_id": null
  },
  "billing_key_id": "key_BWBeN28Vb7cMEx7Ym8AUzs",
  "attribution": "direct",
  "replay_of": null,
  "schedule_id": null,
  "scheduled_at": null,
  "cancel_requested": false,
  "last_outcome": null,
  "last_error_code": null,
  "retryable": null,
  "destination_revision": null,
  "cost": {
    "unit_usd": "0.00002",
    "charged_usd": "0.00002",
    "max_cost_usd": "0.00008",
    "remaining_max_cost_usd": "0.00006",
    "rate_version": "2026-10-20"
  },
  "deleted": false,
  "deleted_at": null,
  "terminal_at": null,
  "created_at": "2026-10-17T09:00:00.000Z",
  "updated_at": "2026-10-17T09:00:00.000Z",
  "deduplicated": true
}

GET/scheduled_jobs/jobs

List jobs

Lists your team's jobs, oldest first, without payloads. Filters: queue_id, state, schedule_id, replay_of, and run_id (the exact run_id metadata value). Costs one operation per page.

ParameterLocationDescription
limitqueryItems per page: 1 to 100, 50 by default. A page also stops before 128 KiB.
cursorqueryThe next_cursor of the previous page. It's bound to your team, the route, and its filters, works for an hour, and doesn't see items created after the first page.
queue_idquery
statequery
schedule_idquery
replay_ofquery
run_idquery
Motor-ModeheaderScheduled jobs is live-only. Omit this header or send live; test gives 422 unsupported_mode before anything else happens.
Responses

200A page of jobs.

{
  "items": [
    {
      "id": "qjb_3VxQ7mK2pR9sLtY4nB6cWd",
      "object": "job",
      "queue_id": "que_8Fq2RkV7xYz3LmN4pQs9Td",
      "state": "queued",
      "reason": null,
      "waiting_reason": null,
      "version": "1",
      "not_before": "2026-10-17T09:00:00.000Z",
      "due_at": "2026-10-17T09:00:00.000Z",
      "deadline_at": "2026-10-18T09:00:00.000Z",
      "attempt_count": 0,
      "max_attempts": 4,
      "affordable_attempts": 4,
      "retry_delays_seconds": [
        10,
        60,
        300
      ],
      "timeout_seconds": 10,
      "lease_until": null,
      "content_type": "application/json",
      "payload_bytes": 21,
      "payload_sha256": "1d5f8c0a6a3c9b3c4e4d2f6c8b1a7e9d0c3b5a7f9e1d3c5b7a9f1e3d5c7b9a1e",
      "payload_retained": true,
      "payload_expires_at": "2026-10-18T09:00:00.000Z",
      "group_id": null,
      "group_sequence": null,
      "deduplication_key": null,
      "deduplication_expires_at": null,
      "metadata": {
        "run_id": "run-7"
      },
      "submitted_by": {
        "key_id": "key_BWBeN28Vb7cMEx7Ym8AUzs",
        "principal_id": null
      },
      "billing_key_id": "key_BWBeN28Vb7cMEx7Ym8AUzs",
      "attribution": "direct",
      "replay_of": null,
      "schedule_id": null,
      "scheduled_at": null,
      "cancel_requested": false,
      "last_outcome": null,
      "last_error_code": null,
      "retryable": null,
      "destination_revision": null,
      "cost": {
        "unit_usd": "0.00002",
        "charged_usd": "0.00002",
        "max_cost_usd": "0.00008",
        "remaining_max_cost_usd": "0.00006",
        "rate_version": "2026-10-20"
      },
      "deleted": false,
      "deleted_at": null,
      "terminal_at": null,
      "created_at": "2026-10-17T09:00:00.000Z",
      "updated_at": "2026-10-17T09:00:00.000Z"
    }
  ],
  "next_cursor": null
}

POST/scheduled_jobs/jobs/batch

Submit jobs in a batch

Accepts up to 10 jobs, to one or several queues, at most 640 KiB of payloads, all or nothing: one refusal refuses every job without a charge. Repeated deduplication keys in a batch give one job, paid once. Each job counts toward the submission rate.

ParameterLocationDescription
Idempotency-Keyheader1 to 128 printable ASCII characters that you choose for this operation. An identical retry returns the first response with Idempotency-Replayed: true for 35 days, without a new charge; a different request with the key is 409 idempotency_conflict, and a retry while the first runs is 409 request_in_progress. A missing key is 400 idempotency_key_required.
Motor-ModeheaderScheduled jobs is live-only. Omit this header or send live; test gives 422 unsupported_mode before anything else happens.
Request body
{
  "jobs": [
    {
      "queue_id": "que_8Fq2RkV7xYz3LmN4pQs9Td",
      "payload_base64": "e30="
    },
    {
      "queue_id": "que_2Tm9QxR4vB7nLpY3sK8cWf",
      "payload_base64": "e30="
    }
  ]
}
FieldTypeDescription
jobsrequiredarray
Responses

202The jobs, in order, and their total cost.

{
  "items": [
    {
      "id": "qjb_3VxQ7mK2pR9sLtY4nB6cWd",
      "object": "job",
      "queue_id": "que_8Fq2RkV7xYz3LmN4pQs9Td",
      "state": "queued",
      "reason": null,
      "waiting_reason": null,
      "version": "1",
      "not_before": "2026-10-17T09:00:00.000Z",
      "due_at": "2026-10-17T09:00:00.000Z",
      "deadline_at": "2026-10-18T09:00:00.000Z",
      "attempt_count": 0,
      "max_attempts": 4,
      "affordable_attempts": 4,
      "retry_delays_seconds": [
        10,
        60,
        300
      ],
      "timeout_seconds": 10,
      "lease_until": null,
      "content_type": "application/json",
      "payload_bytes": 21,
      "payload_sha256": "1d5f8c0a6a3c9b3c4e4d2f6c8b1a7e9d0c3b5a7f9e1d3c5b7a9f1e3d5c7b9a1e",
      "payload_retained": true,
      "payload_expires_at": "2026-10-18T09:00:00.000Z",
      "group_id": null,
      "group_sequence": null,
      "deduplication_key": null,
      "deduplication_expires_at": null,
      "metadata": {
        "run_id": "run-7"
      },
      "submitted_by": {
        "key_id": "key_BWBeN28Vb7cMEx7Ym8AUzs",
        "principal_id": null
      },
      "billing_key_id": "key_BWBeN28Vb7cMEx7Ym8AUzs",
      "attribution": "direct",
      "replay_of": null,
      "schedule_id": null,
      "scheduled_at": null,
      "cancel_requested": false,
      "last_outcome": null,
      "last_error_code": null,
      "retryable": null,
      "destination_revision": null,
      "cost": {
        "unit_usd": "0.00002",
        "charged_usd": "0.00002",
        "max_cost_usd": "0.00008",
        "remaining_max_cost_usd": "0.00006",
        "rate_version": "2026-10-20"
      },
      "deleted": false,
      "deleted_at": null,
      "terminal_at": null,
      "created_at": "2026-10-17T09:00:00.000Z",
      "updated_at": "2026-10-17T09:00:00.000Z",
      "deduplicated": false
    }
  ],
  "total_cost_usd": "0.00002"
}

GET/scheduled_jobs/jobs/{job_id}

Get a job

Returns a job, its pull or handler result while retained, and with include_payload=true, its payload (410 payload_expired once it's gone). A deleted job answers its short record. Costs one operation.

ParameterLocationDescription
job_idpathqjb_...
include_payloadquery
Motor-ModeheaderScheduled jobs is live-only. Omit this header or send live; test gives 422 unsupported_mode before anything else happens.
Responses

200The job.

{
  "id": "qjb_3VxQ7mK2pR9sLtY4nB6cWd",
  "object": "job",
  "queue_id": "que_8Fq2RkV7xYz3LmN4pQs9Td",
  "state": "queued",
  "reason": null,
  "waiting_reason": null,
  "version": "1",
  "not_before": "2026-10-17T09:00:00.000Z",
  "due_at": "2026-10-17T09:00:00.000Z",
  "deadline_at": "2026-10-18T09:00:00.000Z",
  "attempt_count": 0,
  "max_attempts": 4,
  "affordable_attempts": 4,
  "retry_delays_seconds": [
    10,
    60,
    300
  ],
  "timeout_seconds": 10,
  "lease_until": null,
  "content_type": "application/json",
  "payload_bytes": 21,
  "payload_sha256": "1d5f8c0a6a3c9b3c4e4d2f6c8b1a7e9d0c3b5a7f9e1d3c5b7a9f1e3d5c7b9a1e",
  "payload_retained": true,
  "payload_expires_at": "2026-10-18T09:00:00.000Z",
  "group_id": null,
  "group_sequence": null,
  "deduplication_key": null,
  "deduplication_expires_at": null,
  "metadata": {
    "run_id": "run-7"
  },
  "submitted_by": {
    "key_id": "key_BWBeN28Vb7cMEx7Ym8AUzs",
    "principal_id": null
  },
  "billing_key_id": "key_BWBeN28Vb7cMEx7Ym8AUzs",
  "attribution": "direct",
  "replay_of": null,
  "schedule_id": null,
  "scheduled_at": null,
  "cancel_requested": false,
  "last_outcome": null,
  "last_error_code": null,
  "retryable": null,
  "destination_revision": null,
  "cost": {
    "unit_usd": "0.00002",
    "charged_usd": "0.00002",
    "max_cost_usd": "0.00008",
    "remaining_max_cost_usd": "0.00006",
    "rate_version": "2026-10-20"
  },
  "deleted": false,
  "deleted_at": null,
  "terminal_at": null,
  "created_at": "2026-10-17T09:00:00.000Z",
  "updated_at": "2026-10-17T09:00:00.000Z",
  "result": null
}

DELETE/scheduled_jobs/jobs/{job_id}

Delete a job

Cancels the job, erases its payload and result at once, and keeps a short record for 35 days. An attempt already running may still complete its work. A repeated deletion answers the same receipt. No additional charge.

ParameterLocationDescription
job_idpathqjb_...
If-MatchheaderThe resource's ETag, "queues:{id}:{version}". A missing header is 428 precondition_required, and a stale one 412 precondition_failed.
Motor-ModeheaderScheduled jobs is live-only. Omit this header or send live; test gives 422 unsupported_mode before anything else happens.
Responses

202The deletion receipt.

{
  "id": "qjb_3VxQ7mK2pR9sLtY4nB6cWd",
  "object": "job_deletion",
  "deleted": true,
  "deleted_at": "2026-10-17T09:00:00.000Z",
  "state": "canceled",
  "erasure_state": "complete"
}

GET/scheduled_jobs/jobs/{job_id}/attempts

List a job's attempts

Lists a job's attempts in order. Attempt records remain for 35 days after the job finishes. Retained response headers are cleared with the job’s payload after 7 days. Costs one operation.

ParameterLocationDescription
job_idpathqjb_...
Motor-ModeheaderScheduled jobs is live-only. Omit this header or send live; test gives 422 unsupported_mode before anything else happens.
Responses

200The attempts.

{
  "items": [
    {
      "id": "qat_6NpB2xQ8rT4vLmY7kC3sWe",
      "object": "attempt",
      "job_id": "qjb_3VxQ7mK2pR9sLtY4nB6cWd",
      "number": 1,
      "delivery": "http",
      "outcome": "succeeded",
      "http_status": 204,
      "error_code": null,
      "response_headers": {
        "x-request-id": "abc123"
      },
      "destination_revision": "1",
      "destination_host": "agent.example.com",
      "started_at": "2026-10-17T09:00:00.000Z",
      "lease_until": "2026-10-17T09:00:40.000Z",
      "finished_at": "2026-10-17T09:00:00.180Z",
      "duration_ms": 180,
      "charged_usd": "0.00",
      "late_evidence": false
    }
  ],
  "next_cursor": null
}

POST/scheduled_jobs/jobs/{job_id}/cancel

Cancel a job

Immediately marks a waiting job as canceled. If an attempt is running, Motor sets cancel_requested: true and prevents retries. The attempt can finish or run until its lease expires; if it succeeds, the job becomes succeeded. Canceling a finished job answers it unchanged, even with a stale If-Match. No additional charge.

ParameterLocationDescription
job_idpathqjb_...
If-MatchheaderThe resource's ETag, "queues:{id}:{version}". A missing header is 428 precondition_required, and a stale one 412 precondition_failed.
Idempotency-Keyheader1 to 128 printable ASCII characters that you choose for this operation. An identical retry returns the first response with Idempotency-Replayed: true for 35 days, without a new charge; a different request with the key is 409 idempotency_conflict, and a retry while the first runs is 409 request_in_progress. A missing key is 400 idempotency_key_required.
Motor-ModeheaderScheduled jobs is live-only. Omit this header or send live; test gives 422 unsupported_mode before anything else happens.
Request body
{}
Responses

200The job.

{
  "id": "qjb_3VxQ7mK2pR9sLtY4nB6cWd",
  "object": "job",
  "queue_id": "que_8Fq2RkV7xYz3LmN4pQs9Td",
  "state": "queued",
  "reason": null,
  "waiting_reason": null,
  "version": "1",
  "not_before": "2026-10-17T09:00:00.000Z",
  "due_at": "2026-10-17T09:00:00.000Z",
  "deadline_at": "2026-10-18T09:00:00.000Z",
  "attempt_count": 0,
  "max_attempts": 4,
  "affordable_attempts": 4,
  "retry_delays_seconds": [
    10,
    60,
    300
  ],
  "timeout_seconds": 10,
  "lease_until": null,
  "content_type": "application/json",
  "payload_bytes": 21,
  "payload_sha256": "1d5f8c0a6a3c9b3c4e4d2f6c8b1a7e9d0c3b5a7f9e1d3c5b7a9f1e3d5c7b9a1e",
  "payload_retained": true,
  "payload_expires_at": "2026-10-18T09:00:00.000Z",
  "group_id": null,
  "group_sequence": null,
  "deduplication_key": null,
  "deduplication_expires_at": null,
  "metadata": {
    "run_id": "run-7"
  },
  "submitted_by": {
    "key_id": "key_BWBeN28Vb7cMEx7Ym8AUzs",
    "principal_id": null
  },
  "billing_key_id": "key_BWBeN28Vb7cMEx7Ym8AUzs",
  "attribution": "direct",
  "replay_of": null,
  "schedule_id": null,
  "scheduled_at": null,
  "cancel_requested": false,
  "last_outcome": null,
  "last_error_code": null,
  "retryable": null,
  "destination_revision": null,
  "cost": {
    "unit_usd": "0.00002",
    "charged_usd": "0.00002",
    "max_cost_usd": "0.00008",
    "remaining_max_cost_usd": "0.00006",
    "rate_version": "2026-10-20"
  },
  "deleted": false,
  "deleted_at": null,
  "terminal_at": null,
  "created_at": "2026-10-17T09:00:00.000Z",
  "updated_at": "2026-10-17T09:00:00.000Z"
}

POST/scheduled_jobs/jobs/{job_id}/replay

Replay a job

Makes a new paid job from a dead, expired, or canceled job's retained payload, with a new ID and replay_of. If an attempt's outcome was unknown, send allow_duplicate_effects: true: the first effect may have happened (409 duplicate_effects_possible). A succeeded job can't replay.

ParameterLocationDescription
job_idpathqjb_...
If-MatchheaderThe resource's ETag, "queues:{id}:{version}". A missing header is 428 precondition_required, and a stale one 412 precondition_failed.
Idempotency-Keyheader1 to 128 printable ASCII characters that you choose for this operation. An identical retry returns the first response with Idempotency-Replayed: true for 35 days, without a new charge; a different request with the key is 409 idempotency_conflict, and a retry while the first runs is 409 request_in_progress. A missing key is 400 idempotency_key_required.
Motor-ModeheaderScheduled jobs is live-only. Omit this header or send live; test gives 422 unsupported_mode before anything else happens.
Request body
{
  "allow_duplicate_effects": true
}
FieldTypeDescription
queue_idstringAnother queue of your team. The job's group joins a fifo queue's group; an unordered queue drops it.
not_beforestringUTC in RFC 3339, to the millisecond, such as 2026-10-17T09:00:00.000Z.
deadline_atstringUTC in RFC 3339, to the millisecond, such as 2026-10-17T09:00:00.000Z.
max_attemptsinteger
max_cost_usdstringA decimal amount of US dollars, such as 0.00001. Never a binary floating-point number.
allow_duplicate_effectsbooleanRequired when an attempt's outcome was unknown.
Responses

202The new job.

{
  "id": "qjb_3VxQ7mK2pR9sLtY4nB6cWd",
  "object": "job",
  "queue_id": "que_8Fq2RkV7xYz3LmN4pQs9Td",
  "state": "queued",
  "reason": null,
  "waiting_reason": null,
  "version": "1",
  "not_before": "2026-10-17T09:00:00.000Z",
  "due_at": "2026-10-17T09:00:00.000Z",
  "deadline_at": "2026-10-18T09:00:00.000Z",
  "attempt_count": 0,
  "max_attempts": 4,
  "affordable_attempts": 4,
  "retry_delays_seconds": [
    10,
    60,
    300
  ],
  "timeout_seconds": 10,
  "lease_until": null,
  "content_type": "application/json",
  "payload_bytes": 21,
  "payload_sha256": "1d5f8c0a6a3c9b3c4e4d2f6c8b1a7e9d0c3b5a7f9e1d3c5b7a9f1e3d5c7b9a1e",
  "payload_retained": true,
  "payload_expires_at": "2026-10-18T09:00:00.000Z",
  "group_id": null,
  "group_sequence": null,
  "deduplication_key": null,
  "deduplication_expires_at": null,
  "metadata": {
    "run_id": "run-7"
  },
  "submitted_by": {
    "key_id": "key_BWBeN28Vb7cMEx7Ym8AUzs",
    "principal_id": null
  },
  "billing_key_id": "key_BWBeN28Vb7cMEx7Ym8AUzs",
  "attribution": "direct",
  "replay_of": null,
  "schedule_id": null,
  "scheduled_at": null,
  "cancel_requested": false,
  "last_outcome": null,
  "last_error_code": null,
  "retryable": null,
  "destination_revision": null,
  "cost": {
    "unit_usd": "0.00002",
    "charged_usd": "0.00002",
    "max_cost_usd": "0.00008",
    "remaining_max_cost_usd": "0.00006",
    "rate_version": "2026-10-20"
  },
  "deleted": false,
  "deleted_at": null,
  "terminal_at": null,
  "created_at": "2026-10-17T09:00:00.000Z",
  "updated_at": "2026-10-17T09:00:00.000Z",
  "deduplicated": false
}

POST/scheduled_jobs/jobs/{job_id}/resume

Resume a blocked job

Retries a blocked job after checking its key, its access, and its budget again; 409 still_blocked names what still blocks it. No additional charge.

ParameterLocationDescription
job_idpathqjb_...
If-MatchheaderThe resource's ETag, "queues:{id}:{version}". A missing header is 428 precondition_required, and a stale one 412 precondition_failed.
Idempotency-Keyheader1 to 128 printable ASCII characters that you choose for this operation. An identical retry returns the first response with Idempotency-Replayed: true for 35 days, without a new charge; a different request with the key is 409 idempotency_conflict, and a retry while the first runs is 409 request_in_progress. A missing key is 400 idempotency_key_required.
Motor-ModeheaderScheduled jobs is live-only. Omit this header or send live; test gives 422 unsupported_mode before anything else happens.
Request body
{}
Responses

200The job.

{
  "id": "qjb_3VxQ7mK2pR9sLtY4nB6cWd",
  "object": "job",
  "queue_id": "que_8Fq2RkV7xYz3LmN4pQs9Td",
  "state": "queued",
  "reason": null,
  "waiting_reason": null,
  "version": "1",
  "not_before": "2026-10-17T09:00:00.000Z",
  "due_at": "2026-10-17T09:00:00.000Z",
  "deadline_at": "2026-10-18T09:00:00.000Z",
  "attempt_count": 0,
  "max_attempts": 4,
  "affordable_attempts": 4,
  "retry_delays_seconds": [
    10,
    60,
    300
  ],
  "timeout_seconds": 10,
  "lease_until": null,
  "content_type": "application/json",
  "payload_bytes": 21,
  "payload_sha256": "1d5f8c0a6a3c9b3c4e4d2f6c8b1a7e9d0c3b5a7f9e1d3c5b7a9f1e3d5c7b9a1e",
  "payload_retained": true,
  "payload_expires_at": "2026-10-18T09:00:00.000Z",
  "group_id": null,
  "group_sequence": null,
  "deduplication_key": null,
  "deduplication_expires_at": null,
  "metadata": {
    "run_id": "run-7"
  },
  "submitted_by": {
    "key_id": "key_BWBeN28Vb7cMEx7Ym8AUzs",
    "principal_id": null
  },
  "billing_key_id": "key_BWBeN28Vb7cMEx7Ym8AUzs",
  "attribution": "direct",
  "replay_of": null,
  "schedule_id": null,
  "scheduled_at": null,
  "cancel_requested": false,
  "last_outcome": null,
  "last_error_code": null,
  "retryable": null,
  "destination_revision": null,
  "cost": {
    "unit_usd": "0.00002",
    "charged_usd": "0.00002",
    "max_cost_usd": "0.00008",
    "remaining_max_cost_usd": "0.00006",
    "rate_version": "2026-10-20"
  },
  "deleted": false,
  "deleted_at": null,
  "terminal_at": null,
  "created_at": "2026-10-17T09:00:00.000Z",
  "updated_at": "2026-10-17T09:00:00.000Z"
}

POST/scheduled_jobs/jobs/{job_id}/rebind

Rebind a job

Makes the authenticated key the job's billing key and authority for future charges; the submitter and earlier charges stay. A job that a key or billing refusal blocked resumes. A schedule's jobs can't rebind. Costs one operation.

ParameterLocationDescription
job_idpathqjb_...
If-MatchheaderThe resource's ETag, "queues:{id}:{version}". A missing header is 428 precondition_required, and a stale one 412 precondition_failed.
Idempotency-Keyheader1 to 128 printable ASCII characters that you choose for this operation. An identical retry returns the first response with Idempotency-Replayed: true for 35 days, without a new charge; a different request with the key is 409 idempotency_conflict, and a retry while the first runs is 409 request_in_progress. A missing key is 400 idempotency_key_required.
Motor-ModeheaderScheduled jobs is live-only. Omit this header or send live; test gives 422 unsupported_mode before anything else happens.
Request body
{}
Responses

200The job.

{
  "id": "qjb_3VxQ7mK2pR9sLtY4nB6cWd",
  "object": "job",
  "queue_id": "que_8Fq2RkV7xYz3LmN4pQs9Td",
  "state": "queued",
  "reason": null,
  "waiting_reason": null,
  "version": "1",
  "not_before": "2026-10-17T09:00:00.000Z",
  "due_at": "2026-10-17T09:00:00.000Z",
  "deadline_at": "2026-10-18T09:00:00.000Z",
  "attempt_count": 0,
  "max_attempts": 4,
  "affordable_attempts": 4,
  "retry_delays_seconds": [
    10,
    60,
    300
  ],
  "timeout_seconds": 10,
  "lease_until": null,
  "content_type": "application/json",
  "payload_bytes": 21,
  "payload_sha256": "1d5f8c0a6a3c9b3c4e4d2f6c8b1a7e9d0c3b5a7f9e1d3c5b7a9f1e3d5c7b9a1e",
  "payload_retained": true,
  "payload_expires_at": "2026-10-18T09:00:00.000Z",
  "group_id": null,
  "group_sequence": null,
  "deduplication_key": null,
  "deduplication_expires_at": null,
  "metadata": {
    "run_id": "run-7"
  },
  "submitted_by": {
    "key_id": "key_BWBeN28Vb7cMEx7Ym8AUzs",
    "principal_id": null
  },
  "billing_key_id": "key_BWBeN28Vb7cMEx7Ym8AUzs",
  "attribution": "direct",
  "replay_of": null,
  "schedule_id": null,
  "scheduled_at": null,
  "cancel_requested": false,
  "last_outcome": null,
  "last_error_code": null,
  "retryable": null,
  "destination_revision": null,
  "cost": {
    "unit_usd": "0.00002",
    "charged_usd": "0.00002",
    "max_cost_usd": "0.00008",
    "remaining_max_cost_usd": "0.00006",
    "rate_version": "2026-10-20"
  },
  "deleted": false,
  "deleted_at": null,
  "terminal_at": null,
  "created_at": "2026-10-17T09:00:00.000Z",
  "updated_at": "2026-10-17T09:00:00.000Z"
}

POST/scheduled_jobs/validate

Validate a job or schedule

Checks a job or schedule as its submission would, and answers its normalized parameters, its exact prices, a schedule's next ten scheduled times, and warnings. It doesn't store, send, or charge anything, and doesn't need an Idempotency-Key. Motor checks these constraints again when you submit the job or schedule.

ParameterLocationDescription
Motor-ModeheaderScheduled jobs is live-only. Omit this header or send live; test gives 422 unsupported_mode before anything else happens.
Request body
{
  "job": {
    "queue_id": "que_8Fq2RkV7xYz3LmN4pQs9Td",
    "payload_base64": "e30=",
    "max_attempts": 3
  }
}
FieldTypeDescription
jobobject
scheduleobject
Responses

200The checked job or schedule.

Pull workers

A pull queue keeps its jobs until your workers lease them, so a job can take up to an hour and your workers don't need a public endpoint. Give workers a key of their own that only allows Scheduled jobs, with a monthly spending limit.

curl https://api.motor.dev/scheduled_jobs/queues/que_.../leases \
  -H "Authorization: Bearer $WORKER_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"max_jobs": 5, "lease_seconds": 120}'

Each item has the job, its payload_base64, a receipt (qlr_...), and lease_until. An empty answer has retry_after_seconds: wait that long before you poll again. If you lose a lease response, retry with the same Idempotency-Key while the leases remain active. After a lease ends, the retry returns 409 lease_lost.

  • POST /jobs/{job_id}/heartbeat with receipt and extend_seconds extends a lease at most once every 10 seconds. The lease cannot extend past the job’s deadline or one hour after the attempt started, whichever comes first. It never shortens a lease. If someone canceled the job, it answers cancel_requested: true and doesn't extend: stop.
  • POST /jobs/{job_id}/ack with receipt and an optional compact JSON result of at most 4 KiB finishes the job. Motor records your declaration; it doesn't check your downstream work.
  • POST /jobs/{job_id}/nack with receipt, retryable, and an error_code such as rate_limited reports a failed attempt: Motor retries it under the job's policy or marks the job dead. A cancellation request ends it as canceled.

A receipt works only with the key that leased it, for its own job and attempt. A wrong, expired, or superseded receipt gets 409 lease_lost and changes nothing: don't commit that job's effect.

Run long jobs on pull workers

Create a pull queue for jobs longer than 30 seconds, then submit a job using its queue ID as in the first example.

curl -i "https://api.motor.dev/scheduled_jobs/queues" \
  -H "Authorization: Bearer $MOTOR_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"name": "Long jobs", "delivery": "pull"}'
HTTP 201

{
  "id": "que_pull",
  "delivery": "pull"
}
curl -i "https://api.motor.dev/scheduled_jobs/jobs" \
  -H "Authorization: Bearer $MOTOR_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"queue_id": "que_pull", "payload_base64": "e30="}'
HTTP 202

{
  "id": "qjb_long",
  "state": "queued"
}
curl -i "https://api.motor.dev/scheduled_jobs/queues/que_pull/leases" \
  -H "Authorization: Bearer $MOTOR_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"max_jobs": 1, "lease_seconds": 120}'
HTTP 200

{
  "items": [
    {
      "job": {
        "id": "qjb_long",
        "state": "leased"
      },
      "receipt": "qlr_...",
      "lease_until": "2026-11-01T12:02:00Z",
      "payload_base64": "e30="
    }
  ],
  "retry_after_seconds": 0
}

Before the lease expires, extend it with the same worker key. Send heartbeats no more than once every 10 seconds; stop if cancel_requested is true.

curl -i "https://api.motor.dev/scheduled_jobs/jobs/qjb_long/heartbeat" \
  -H "Authorization: Bearer $MOTOR_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"receipt": "qlr_...", "extend_seconds": 120}'
HTTP 200

{
  "job_id": "qjb_long",
  "lease_until": "2026-11-01T12:04:00Z",
  "extended": true,
  "cancel_requested": false
}

If the provider rate-limits this attempt, report a retryable failure:

curl -i "https://api.motor.dev/scheduled_jobs/jobs/qjb_long/nack" \
  -H "Authorization: Bearer $MOTOR_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"receipt": "qlr_...", "retryable": true, "error_code": "rate_limited"}'
HTTP 200

{
  "id": "qjb_long",
  "state": "retry_wait",
  "last_error_code": "rate_limited"
}

A nack uses the queue’s retry policy; it doesn’t accept Retry-After. On success use ack instead. A lease can extend at most an hour from the attempt’s start.

POST/scheduled_jobs/queues/{queue_id}/leases

Lease jobs

Leases at most max_jobs due jobs of a pull queue for lease_seconds, in order of their time and submission, at most 128 KiB in all. Each lease has a receipt, which works only with this key, and the job's payload. A due retry costs one dispatch unit; one that the key can't afford is blocked and counted in blocked_count. If you lose the lease response, retry with the same Idempotency-Key while its leases are active. After a lease ends, the retry returns 409 lease_lost. Each call costs one operation, an empty poll too.

ParameterLocationDescription
queue_idpathque_...
Idempotency-Keyheader1 to 128 printable ASCII characters that you choose for this operation. An identical retry returns the first response with Idempotency-Replayed: true for 35 days, without a new charge; a different request with the key is 409 idempotency_conflict, and a retry while the first runs is 409 request_in_progress. A missing key is 400 idempotency_key_required.
Motor-ModeheaderScheduled jobs is live-only. Omit this header or send live; test gives 422 unsupported_mode before anything else happens.
Request body
{
  "max_jobs": 5,
  "lease_seconds": 120
}
FieldTypeDescription
max_jobsinteger
lease_secondsinteger
Responses

200The leases, or none and when to poll again.

{
  "items": [
    {
      "job": {
        "id": "qjb_3VxQ7mK2pR9sLtY4nB6cWd",
        "object": "job",
        "queue_id": "que_2Tm9QxR4vB7nLpY3sK8cWf",
        "state": "leased",
        "reason": null,
        "waiting_reason": null,
        "version": "2",
        "not_before": "2026-10-17T09:00:00.000Z",
        "due_at": null,
        "deadline_at": "2026-10-18T09:00:00.000Z",
        "attempt_count": 1,
        "max_attempts": 4,
        "affordable_attempts": 4,
        "retry_delays_seconds": [
          10,
          60,
          300
        ],
        "timeout_seconds": null,
        "lease_until": "2026-10-17T09:02:00.000Z",
        "content_type": "application/json",
        "payload_bytes": 21,
        "payload_sha256": "1d5f8c0a6a3c9b3c4e4d2f6c8b1a7e9d0c3b5a7f9e1d3c5b7a9f1e3d5c7b9a1e",
        "payload_retained": true,
        "payload_expires_at": "2026-10-18T09:00:00.000Z",
        "group_id": null,
        "group_sequence": null,
        "deduplication_key": null,
        "deduplication_expires_at": null,
        "metadata": {
          "run_id": "run-7"
        },
        "submitted_by": {
          "key_id": "key_BWBeN28Vb7cMEx7Ym8AUzs",
          "principal_id": null
        },
        "billing_key_id": "key_BWBeN28Vb7cMEx7Ym8AUzs",
        "attribution": "direct",
        "replay_of": null,
        "schedule_id": null,
        "scheduled_at": null,
        "cancel_requested": false,
        "last_outcome": null,
        "last_error_code": null,
        "retryable": null,
        "destination_revision": null,
        "cost": {
          "unit_usd": "0.00002",
          "charged_usd": "0.00002",
          "max_cost_usd": "0.00008",
          "remaining_max_cost_usd": "0.00006",
          "rate_version": "2026-10-20"
        },
        "deleted": false,
        "deleted_at": null,
        "terminal_at": null,
        "created_at": "2026-10-17T09:00:00.000Z",
        "updated_at": "2026-10-17T09:00:00.000Z"
      },
      "attempt_id": "qat_6NpB2xQ8rT4vLmY7kC3sWe",
      "attempt_number": 1,
      "receipt": "qlr_4fRm8NqZ2kLpVx7cJs9hYb3TnW6dXe5gHu1",
      "lease_until": "2026-10-17T09:02:00.000Z",
      "payload_base64": "eyJpbnZvaWNlIjogImludl80MiJ9",
      "content_type": "application/json"
    }
  ],
  "retry_after_seconds": 0,
  "blocked_count": 0
}

POST/scheduled_jobs/jobs/{job_id}/heartbeat

Extend a lease

Extends a lease to extend_seconds from now, without exceeding the job’s deadline or one hour after the attempt started, whichever comes first. It never shortens a lease, and extends a job at most once each 10 seconds (429). A job with a cancellation request isn't extended. Costs one operation.

ParameterLocationDescription
job_idpathqjb_...
Idempotency-Keyheader1 to 128 printable ASCII characters that you choose for this operation. An identical retry returns the first response with Idempotency-Replayed: true for 35 days, without a new charge; a different request with the key is 409 idempotency_conflict, and a retry while the first runs is 409 request_in_progress. A missing key is 400 idempotency_key_required.
Motor-ModeheaderScheduled jobs is live-only. Omit this header or send live; test gives 422 unsupported_mode before anything else happens.
Request body
{
  "receipt": "qlr_4fRm8NqZ2kLpVx7cJs9hYb3TnW6dXe5gHu1",
  "extend_seconds": 120
}
FieldTypeDescription
receiptrequiredstring
extend_secondsinteger
Responses

200The lease's new end.

{
  "job_id": "qjb_3VxQ7mK2pR9sLtY4nB6cWd",
  "attempt_id": "qat_6NpB2xQ8rT4vLmY7kC3sWe",
  "lease_until": "2026-10-17T09:04:00.000Z",
  "extended": true,
  "cancel_requested": false
}

POST/scheduled_jobs/jobs/{job_id}/ack

Acknowledge a lease

Finishes the job as succeeded, with an optional compact JSON result of at most 4 KiB. Motor records your declaration and doesn't check your downstream effect. A wrong, expired, or superseded receipt is 409 lease_lost and changes nothing. No additional charge.

ParameterLocationDescription
job_idpathqjb_...
Idempotency-Keyheader1 to 128 printable ASCII characters that you choose for this operation. An identical retry returns the first response with Idempotency-Replayed: true for 35 days, without a new charge; a different request with the key is 409 idempotency_conflict, and a retry while the first runs is 409 request_in_progress. A missing key is 400 idempotency_key_required.
Motor-ModeheaderScheduled jobs is live-only. Omit this header or send live; test gives 422 unsupported_mode before anything else happens.
Request body
{
  "receipt": "qlr_4fRm8NqZ2kLpVx7cJs9hYb3TnW6dXe5gHu1",
  "result": {
    "width": 640
  }
}
FieldTypeDescription
receiptrequiredstring
resultAny JSON value.
Responses

200The job.

{
  "id": "qjb_3VxQ7mK2pR9sLtY4nB6cWd",
  "object": "job",
  "queue_id": "que_8Fq2RkV7xYz3LmN4pQs9Td",
  "state": "succeeded",
  "reason": null,
  "waiting_reason": null,
  "version": "1",
  "not_before": "2026-10-17T09:00:00.000Z",
  "due_at": "2026-10-17T09:00:00.000Z",
  "deadline_at": "2026-10-18T09:00:00.000Z",
  "attempt_count": 0,
  "max_attempts": 4,
  "affordable_attempts": 4,
  "retry_delays_seconds": [
    10,
    60,
    300
  ],
  "timeout_seconds": 10,
  "lease_until": null,
  "content_type": "application/json",
  "payload_bytes": 21,
  "payload_sha256": "1d5f8c0a6a3c9b3c4e4d2f6c8b1a7e9d0c3b5a7f9e1d3c5b7a9f1e3d5c7b9a1e",
  "payload_retained": true,
  "payload_expires_at": "2026-10-18T09:00:00.000Z",
  "group_id": null,
  "group_sequence": null,
  "deduplication_key": null,
  "deduplication_expires_at": null,
  "metadata": {
    "run_id": "run-7"
  },
  "submitted_by": {
    "key_id": "key_BWBeN28Vb7cMEx7Ym8AUzs",
    "principal_id": null
  },
  "billing_key_id": "key_BWBeN28Vb7cMEx7Ym8AUzs",
  "attribution": "direct",
  "replay_of": null,
  "schedule_id": null,
  "scheduled_at": null,
  "cancel_requested": false,
  "last_outcome": null,
  "last_error_code": null,
  "retryable": null,
  "destination_revision": null,
  "cost": {
    "unit_usd": "0.00002",
    "charged_usd": "0.00002",
    "max_cost_usd": "0.00008",
    "remaining_max_cost_usd": "0.00006",
    "rate_version": "2026-10-20"
  },
  "deleted": false,
  "deleted_at": null,
  "terminal_at": null,
  "created_at": "2026-10-17T09:00:00.000Z",
  "updated_at": "2026-10-17T09:00:00.000Z"
}

POST/scheduled_jobs/jobs/{job_id}/nack

Report a failed attempt

Reports a failed attempt: retryable: true applies the retry policy, and false marks the job dead with error_code. If cancellation was requested, the job ends as canceled. No additional charge.

ParameterLocationDescription
job_idpathqjb_...
Idempotency-Keyheader1 to 128 printable ASCII characters that you choose for this operation. An identical retry returns the first response with Idempotency-Replayed: true for 35 days, without a new charge; a different request with the key is 409 idempotency_conflict, and a retry while the first runs is 409 request_in_progress. A missing key is 400 idempotency_key_required.
Motor-ModeheaderScheduled jobs is live-only. Omit this header or send live; test gives 422 unsupported_mode before anything else happens.
Request body
{
  "receipt": "qlr_4fRm8NqZ2kLpVx7cJs9hYb3TnW6dXe5gHu1",
  "retryable": true,
  "error_code": "rate_limited"
}
FieldTypeDescription
receiptrequiredstring
retryablerequiredboolean
error_codestring
Responses

200The job.

{
  "id": "qjb_3VxQ7mK2pR9sLtY4nB6cWd",
  "object": "job",
  "queue_id": "que_8Fq2RkV7xYz3LmN4pQs9Td",
  "state": "retry_wait",
  "reason": null,
  "waiting_reason": null,
  "version": "1",
  "not_before": "2026-10-17T09:00:00.000Z",
  "due_at": "2026-10-17T09:00:00.000Z",
  "deadline_at": "2026-10-18T09:00:00.000Z",
  "attempt_count": 0,
  "max_attempts": 4,
  "affordable_attempts": 4,
  "retry_delays_seconds": [
    10,
    60,
    300
  ],
  "timeout_seconds": 10,
  "lease_until": null,
  "content_type": "application/json",
  "payload_bytes": 21,
  "payload_sha256": "1d5f8c0a6a3c9b3c4e4d2f6c8b1a7e9d0c3b5a7f9e1d3c5b7a9f1e3d5c7b9a1e",
  "payload_retained": true,
  "payload_expires_at": "2026-10-18T09:00:00.000Z",
  "group_id": null,
  "group_sequence": null,
  "deduplication_key": null,
  "deduplication_expires_at": null,
  "metadata": {
    "run_id": "run-7"
  },
  "submitted_by": {
    "key_id": "key_BWBeN28Vb7cMEx7Ym8AUzs",
    "principal_id": null
  },
  "billing_key_id": "key_BWBeN28Vb7cMEx7Ym8AUzs",
  "attribution": "direct",
  "replay_of": null,
  "schedule_id": null,
  "scheduled_at": null,
  "cancel_requested": false,
  "last_outcome": null,
  "last_error_code": null,
  "retryable": null,
  "destination_revision": null,
  "cost": {
    "unit_usd": "0.00002",
    "charged_usd": "0.00002",
    "max_cost_usd": "0.00008",
    "remaining_max_cost_usd": "0.00006",
    "rate_version": "2026-10-20"
  },
  "deleted": false,
  "deleted_at": null,
  "terminal_at": null,
  "created_at": "2026-10-17T09:00:00.000Z",
  "updated_at": "2026-10-17T09:00:00.000Z"
}

Schedules

A schedule submits a job template on a timer: a five-field cron expression or a macro such as @daily in an IANA timezone, or interval_seconds (60 to 604,800) from start_at. Every schedule has an end_at at most 366 days away and a max_total_cost_usd for its dispatch and schedule-days.

curl https://api.motor.dev/scheduled_jobs/schedules \
  -H "Authorization: Bearer $MOTOR_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "queue_id": "que_...",
    "template": {"payload_base64": "eyJyZXBvcnQiOiAiZGFpbHkifQ==", "max_attempts": 2},
    "cron": "0 9 * * MON-FRI",
    "timezone": "America/New_York",
    "end_at": "2027-01-01T00:00:00Z",
    "max_total_cost_usd": "0.50"
  }'

That schedule can spend at most $0.50 on dispatch and schedule-days. Creation and other operations are charged separately. Each schedule-day costs $0.001, and each occurrence reserves its job's maximum dispatch cost: here, two attempts at $0.00002 each. The response lists next_instants in UTC. In a daylight saving change, a time in the gap fires shifted by the gap, and a repeated time fires once, at its earlier instant.

  • misfire_policy: skip (the default) drops occurrences missed by more than a minute; latest admits only the newest missed one; catch_up admits each missed one whose deadline allows, 100 at a time.
  • overlap_policy: allow (the default), or skip to skip an occurrence while an earlier one's job hasn't finished.
  • The template's deadline_seconds counts from each scheduled instant. An occurrence whose deadline passed is skipped.

Update a schedule with PATCH and If-Match. Timing and template changes apply only to future runs; missed runs aren’t added. paused: false resumes with the next scheduled run after the update. A paused schedule still incurs daily charges, so delete a schedule that you don't need. Deleting stops its occurrences and keeps their jobs, unless you send ?cancel_pending=true.

Schedule recurring work with a cost cap

Run an hourly task while limiting combined dispatch and schedule-day charges. Choose an end_at in the future, no more than 366 days away.

curl -i "https://api.motor.dev/scheduled_jobs/schedules" \
  -H "Authorization: Bearer $MOTOR_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"queue_id": "que_...", "template": {"payload_base64": "e30=", "max_attempts": 2}, "interval_seconds": 3600, "end_at": "2026-12-01T00:00:00Z", "max_total_cost_usd": "0.50"}'
HTTP 201

{
  "id": "qsc_...",
  "max_total_cost_usd": "0.50",
  "next_instants": [
    "2026-11-01T13:00:00Z"
  ]
}

Creation and other API operations are charged separately. Paused schedules still accrue schedule-day charges.

POST/scheduled_jobs/schedules

Create a schedule

Creates a recurring schedule of a job template on a cron expression in a time zone, or an interval from start_at. It charges the first schedule-day ($0.001) and one operation. Each occurrence reserves its job's maximum cost against max_total_cost_usd, which also pays schedule-days.

ParameterLocationDescription
Idempotency-Keyheader1 to 128 printable ASCII characters that you choose for this operation. An identical retry returns the first response with Idempotency-Replayed: true for 35 days, without a new charge; a different request with the key is 409 idempotency_conflict, and a retry while the first runs is 409 request_in_progress. A missing key is 400 idempotency_key_required.
Motor-ModeheaderScheduled jobs is live-only. Omit this header or send live; test gives 422 unsupported_mode before anything else happens.
Request body
{
  "queue_id": "que_8Fq2RkV7xYz3LmN4pQs9Td",
  "template": {
    "payload_base64": "eyJyZXBvcnQiOiAiZGFpbHkifQ==",
    "max_attempts": 2
  },
  "cron": "0 9 * * MON-FRI",
  "timezone": "America/New_York",
  "end_at": "2027-01-01T00:00:00Z",
  "max_total_cost_usd": "0.50"
}
FieldTypeDescription
queue_idrequiredstring
templaterequiredobject
cronstringFive fields or a macro such as @daily. Exactly one of cron and interval_seconds.
interval_secondsinteger
timezonestringAn IANA time zone, for cron only.
start_atNow by default. An interval counts from it.
end_atrequiredMust be after start_at and no more than 366 days after schedule creation.
pausedboolean
misfire_policystring
overlap_policystring
max_total_cost_usdrequiredThe maximum combined cost of dispatch and schedule-days: at least $0.001.
max_occurrencesinteger
Responses

201The schedule and its next ten scheduled times.

{
  "id": "qsc_7KpR3xN9vT2mLqY5sB8cWd",
  "object": "schedule",
  "queue_id": "que_8Fq2RkV7xYz3LmN4pQs9Td",
  "status": "active",
  "reason": null,
  "template": {
    "content_type": "application/json",
    "payload_bytes": 19,
    "payload_sha256": "5a3c9b1e7d2f4c6a8b0d1e3f5a7c9b2d4f6e8a0c1b3d5f7e9a2c4b6d8f0e1a3c",
    "payload_retained": true,
    "group_id": null,
    "metadata": {},
    "max_attempts": 2,
    "max_cost_usd": "0.00004",
    "deadline_seconds": 86400
  },
  "cron": "0 9 * * 1-5",
  "timezone": "America/New_York",
  "interval_seconds": null,
  "start_at": "2026-10-17T09:00:00.000Z",
  "end_at": "2027-01-01T00:00:00.000Z",
  "paused": false,
  "misfire_policy": "skip",
  "overlap_policy": "allow",
  "max_total_cost_usd": "0.50",
  "max_occurrences": 1000000,
  "occurrences": 0,
  "skipped": {
    "count": 0,
    "last_at": null,
    "last_reason": null
  },
  "budget": {
    "spent_usd": "0.001",
    "reserved_usd": "0.00",
    "remaining_usd": "0.499"
  },
  "periods": 1,
  "paid_until": "2026-10-18T09:00:00.000Z",
  "rates": {
    "schedule_day_usd": "0.001",
    "dispatch_usd": "0.00002",
    "rate_version": "2026-10-20"
  },
  "billing_key_id": "key_BWBeN28Vb7cMEx7Ym8AUzs",
  "generation": "1",
  "next_instants": [
    "2026-10-19T13:00:00.000Z",
    "2026-10-20T13:00:00.000Z",
    "2026-10-21T13:00:00.000Z"
  ],
  "erase_at": null,
  "version": "1",
  "created_at": "2026-10-17T09:00:00.000Z",
  "updated_at": "2026-10-17T09:00:00.000Z",
  "completed_at": null
}

GET/scheduled_jobs/schedules

List schedules

Lists your team's schedules, oldest first. Filters: queue_id and status. Costs one operation per page.

ParameterLocationDescription
limitqueryItems per page: 1 to 100, 50 by default. A page also stops before 128 KiB.
cursorqueryThe next_cursor of the previous page. It's bound to your team, the route, and its filters, works for an hour, and doesn't see items created after the first page.
queue_idquery
statusquery
Motor-ModeheaderScheduled jobs is live-only. Omit this header or send live; test gives 422 unsupported_mode before anything else happens.
Responses

200A page of schedules.

{
  "items": [
    {
      "id": "qsc_7KpR3xN9vT2mLqY5sB8cWd",
      "object": "schedule",
      "queue_id": "que_8Fq2RkV7xYz3LmN4pQs9Td",
      "status": "active",
      "reason": null,
      "template": {
        "content_type": "application/json",
        "payload_bytes": 19,
        "payload_sha256": "5a3c9b1e7d2f4c6a8b0d1e3f5a7c9b2d4f6e8a0c1b3d5f7e9a2c4b6d8f0e1a3c",
        "payload_retained": true,
        "group_id": null,
        "metadata": {},
        "max_attempts": 2,
        "max_cost_usd": "0.00004",
        "deadline_seconds": 86400
      },
      "cron": "0 9 * * 1-5",
      "timezone": "America/New_York",
      "interval_seconds": null,
      "start_at": "2026-10-17T09:00:00.000Z",
      "end_at": "2027-01-01T00:00:00.000Z",
      "paused": false,
      "misfire_policy": "skip",
      "overlap_policy": "allow",
      "max_total_cost_usd": "0.50",
      "max_occurrences": 1000000,
      "occurrences": 0,
      "skipped": {
        "count": 0,
        "last_at": null,
        "last_reason": null
      },
      "budget": {
        "spent_usd": "0.001",
        "reserved_usd": "0.00",
        "remaining_usd": "0.499"
      },
      "periods": 1,
      "paid_until": "2026-10-18T09:00:00.000Z",
      "rates": {
        "schedule_day_usd": "0.001",
        "dispatch_usd": "0.00002",
        "rate_version": "2026-10-20"
      },
      "billing_key_id": "key_BWBeN28Vb7cMEx7Ym8AUzs",
      "generation": "1",
      "next_instants": [
        "2026-10-19T13:00:00.000Z",
        "2026-10-20T13:00:00.000Z",
        "2026-10-21T13:00:00.000Z"
      ],
      "erase_at": null,
      "version": "1",
      "created_at": "2026-10-17T09:00:00.000Z",
      "updated_at": "2026-10-17T09:00:00.000Z",
      "completed_at": null
    }
  ],
  "next_cursor": null
}

GET/scheduled_jobs/schedules/{schedule_id}

Get a schedule

Returns a schedule and its next ten scheduled times in UTC. Costs one operation.

ParameterLocationDescription
schedule_idpathqsc_...
Motor-ModeheaderScheduled jobs is live-only. Omit this header or send live; test gives 422 unsupported_mode before anything else happens.
Responses

200The schedule.

{
  "id": "qsc_7KpR3xN9vT2mLqY5sB8cWd",
  "object": "schedule",
  "queue_id": "que_8Fq2RkV7xYz3LmN4pQs9Td",
  "status": "active",
  "reason": null,
  "template": {
    "content_type": "application/json",
    "payload_bytes": 19,
    "payload_sha256": "5a3c9b1e7d2f4c6a8b0d1e3f5a7c9b2d4f6e8a0c1b3d5f7e9a2c4b6d8f0e1a3c",
    "payload_retained": true,
    "group_id": null,
    "metadata": {},
    "max_attempts": 2,
    "max_cost_usd": "0.00004",
    "deadline_seconds": 86400
  },
  "cron": "0 9 * * 1-5",
  "timezone": "America/New_York",
  "interval_seconds": null,
  "start_at": "2026-10-17T09:00:00.000Z",
  "end_at": "2027-01-01T00:00:00.000Z",
  "paused": false,
  "misfire_policy": "skip",
  "overlap_policy": "allow",
  "max_total_cost_usd": "0.50",
  "max_occurrences": 1000000,
  "occurrences": 0,
  "skipped": {
    "count": 0,
    "last_at": null,
    "last_reason": null
  },
  "budget": {
    "spent_usd": "0.001",
    "reserved_usd": "0.00",
    "remaining_usd": "0.499"
  },
  "periods": 1,
  "paid_until": "2026-10-18T09:00:00.000Z",
  "rates": {
    "schedule_day_usd": "0.001",
    "dispatch_usd": "0.00002",
    "rate_version": "2026-10-20"
  },
  "billing_key_id": "key_BWBeN28Vb7cMEx7Ym8AUzs",
  "generation": "1",
  "next_instants": [
    "2026-10-19T13:00:00.000Z",
    "2026-10-20T13:00:00.000Z",
    "2026-10-21T13:00:00.000Z"
  ],
  "erase_at": null,
  "version": "1",
  "created_at": "2026-10-17T09:00:00.000Z",
  "updated_at": "2026-10-17T09:00:00.000Z",
  "completed_at": null
}

PATCH/scheduled_jobs/schedules/{schedule_id}

Update a schedule

Changes the fields that you send. Timing and template changes apply only to future runs; missed runs aren’t added. paused: false resumes strictly after now. Any change of a blocked schedule checks it again and, if its paid time ended, starts a new prepaid day from now. max_total_cost_usd can't drop below what the schedule spent and reserved. A completed schedule can't change. Costs one operation.

ParameterLocationDescription
schedule_idpathqsc_...
If-MatchheaderThe resource's ETag, "queues:{id}:{version}". A missing header is 428 precondition_required, and a stale one 412 precondition_failed.
Motor-ModeheaderScheduled jobs is live-only. Omit this header or send live; test gives 422 unsupported_mode before anything else happens.
Request body
{
  "paused": true
}
FieldTypeDescription
templateobject
cronstringFive fields or a macro such as @daily. Exactly one of cron and interval_seconds.
interval_secondsinteger
timezonestringAn IANA time zone, for cron only.
start_atThe interval anchor. Omit it to keep the current start time.
end_atMust be after start_at and no more than 366 days after schedule creation.
pausedboolean
misfire_policystring
overlap_policystring
max_total_cost_usdThe maximum combined cost of dispatch and schedule-days: at least $0.001.
max_occurrencesinteger
Responses

200The schedule.

{
  "id": "qsc_7KpR3xN9vT2mLqY5sB8cWd",
  "object": "schedule",
  "queue_id": "que_8Fq2RkV7xYz3LmN4pQs9Td",
  "status": "active",
  "reason": null,
  "template": {
    "content_type": "application/json",
    "payload_bytes": 19,
    "payload_sha256": "5a3c9b1e7d2f4c6a8b0d1e3f5a7c9b2d4f6e8a0c1b3d5f7e9a2c4b6d8f0e1a3c",
    "payload_retained": true,
    "group_id": null,
    "metadata": {},
    "max_attempts": 2,
    "max_cost_usd": "0.00004",
    "deadline_seconds": 86400
  },
  "cron": "0 9 * * 1-5",
  "timezone": "America/New_York",
  "interval_seconds": null,
  "start_at": "2026-10-17T09:00:00.000Z",
  "end_at": "2027-01-01T00:00:00.000Z",
  "paused": false,
  "misfire_policy": "skip",
  "overlap_policy": "allow",
  "max_total_cost_usd": "0.50",
  "max_occurrences": 1000000,
  "occurrences": 0,
  "skipped": {
    "count": 0,
    "last_at": null,
    "last_reason": null
  },
  "budget": {
    "spent_usd": "0.001",
    "reserved_usd": "0.00",
    "remaining_usd": "0.499"
  },
  "periods": 1,
  "paid_until": "2026-10-18T09:00:00.000Z",
  "rates": {
    "schedule_day_usd": "0.001",
    "dispatch_usd": "0.00002",
    "rate_version": "2026-10-20"
  },
  "billing_key_id": "key_BWBeN28Vb7cMEx7Ym8AUzs",
  "generation": "1",
  "next_instants": [
    "2026-10-19T13:00:00.000Z",
    "2026-10-20T13:00:00.000Z",
    "2026-10-21T13:00:00.000Z"
  ],
  "erase_at": null,
  "version": "1",
  "created_at": "2026-10-17T09:00:00.000Z",
  "updated_at": "2026-10-17T09:00:00.000Z",
  "completed_at": null
}

DELETE/scheduled_jobs/schedules/{schedule_id}

Delete a schedule

Stops future occurrences at once and erases the template. Admitted jobs stay unless you send cancel_pending=true. A begun schedule-day isn't refunded. No additional charge.

ParameterLocationDescription
schedule_idpathqsc_...
If-MatchheaderThe resource's ETag, "queues:{id}:{version}". A missing header is 428 precondition_required, and a stale one 412 precondition_failed.
cancel_pendingquery
Motor-ModeheaderScheduled jobs is live-only. Omit this header or send live; test gives 422 unsupported_mode before anything else happens.
Responses

202The deletion receipt.

{
  "id": "qsc_7KpR3xN9vT2mLqY5sB8cWd",
  "object": "schedule_deletion",
  "deleted": true,
  "deleted_at": "2026-10-17T09:00:00.000Z",
  "cancel_pending": false,
  "canceled_jobs": 0
}

POST/scheduled_jobs/schedules/{schedule_id}/rebind

Rebind a schedule

Makes the authenticated key the schedule's billing key for future occurrences and schedule-days; accepted jobs keep their key. A schedule that a key or billing refusal blocked is checked again and resumes. Costs one operation.

ParameterLocationDescription
schedule_idpathqsc_...
If-MatchheaderThe resource's ETag, "queues:{id}:{version}". A missing header is 428 precondition_required, and a stale one 412 precondition_failed.
Idempotency-Keyheader1 to 128 printable ASCII characters that you choose for this operation. An identical retry returns the first response with Idempotency-Replayed: true for 35 days, without a new charge; a different request with the key is 409 idempotency_conflict, and a retry while the first runs is 409 request_in_progress. A missing key is 400 idempotency_key_required.
Motor-ModeheaderScheduled jobs is live-only. Omit this header or send live; test gives 422 unsupported_mode before anything else happens.
Request body
{}
Responses

200The schedule.

{
  "id": "qsc_7KpR3xN9vT2mLqY5sB8cWd",
  "object": "schedule",
  "queue_id": "que_8Fq2RkV7xYz3LmN4pQs9Td",
  "status": "active",
  "reason": null,
  "template": {
    "content_type": "application/json",
    "payload_bytes": 19,
    "payload_sha256": "5a3c9b1e7d2f4c6a8b0d1e3f5a7c9b2d4f6e8a0c1b3d5f7e9a2c4b6d8f0e1a3c",
    "payload_retained": true,
    "group_id": null,
    "metadata": {},
    "max_attempts": 2,
    "max_cost_usd": "0.00004",
    "deadline_seconds": 86400
  },
  "cron": "0 9 * * 1-5",
  "timezone": "America/New_York",
  "interval_seconds": null,
  "start_at": "2026-10-17T09:00:00.000Z",
  "end_at": "2027-01-01T00:00:00.000Z",
  "paused": false,
  "misfire_policy": "skip",
  "overlap_policy": "allow",
  "max_total_cost_usd": "0.50",
  "max_occurrences": 1000000,
  "occurrences": 0,
  "skipped": {
    "count": 0,
    "last_at": null,
    "last_reason": null
  },
  "budget": {
    "spent_usd": "0.001",
    "reserved_usd": "0.00",
    "remaining_usd": "0.499"
  },
  "periods": 1,
  "paid_until": "2026-10-18T09:00:00.000Z",
  "rates": {
    "schedule_day_usd": "0.001",
    "dispatch_usd": "0.00002",
    "rate_version": "2026-10-20"
  },
  "billing_key_id": "key_BWBeN28Vb7cMEx7Ym8AUzs",
  "generation": "1",
  "next_instants": [
    "2026-10-19T13:00:00.000Z",
    "2026-10-20T13:00:00.000Z",
    "2026-10-21T13:00:00.000Z"
  ],
  "erase_at": null,
  "version": "1",
  "created_at": "2026-10-17T09:00:00.000Z",
  "updated_at": "2026-10-17T09:00:00.000Z",
  "completed_at": null
}