HTTP API
Base URL: http://localhost:<PORT> (example uses 9000).
All /api/jobs/* routes need:
Content-Type: application/json- Signature header (see Request signing) over the exact request body
GET /api/health needs no signature.
Which scheduling path
| Need | Use | Recurs? |
|---|---|---|
| Fixed recurring work (daily digest, nightly cleanup) | YAML cron_jobs in Configuration | Yes, until you change config and restart |
| Run once, now or at a known timestamp | POST /api/jobs/one-off (optional runAt) | No |
| Run once at the next match of a cron expression | POST /api/jobs/schedule | No. Call again if you need another occurrence |
Examples: publish a post at 15:00 tomorrow → one-off + runAt. Send a digest every day at 08:00 → YAML cron_jobs. “Next Monday 08:00 only” from app logic → /schedule.
Health
curl -s http://127.0.0.1:9000/api/health{ "ok": true }503 with { "ok": false } if pg-boss is not ready.
One-off jobs
POST /api/jobs/one-off → 202
Body: non-empty array.
| Field | Required | Notes |
|---|---|---|
idempotencyKey | Yes | string |
queue | Yes | must exist in YAML |
payload | No | object, default {} |
runAt | No | ISO-8601 with offset. Future → delayed; past/omitted → now |
Run now
[
{
"idempotencyKey": "invoice-42-send",
"queue": "WEBHOOK_DELIVERY",
"payload": { "invoiceId": 42, "action": "send" }
}
]Response:
{ "jobs": [{ "jobId": "pg-boss-job-id" }] }Delayed
[
{
"idempotencyKey": "invoice-42-reminder",
"queue": "WEBHOOK_DELIVERY",
"payload": { "invoiceId": 42, "action": "remind" },
"runAt": "2026-03-23T14:30:00Z"
}
]Batch
You can send multiple items in one array. They are processed sequentially. If item 2 fails validation or enqueue, earlier items in that request may already be stored.
[
{
"idempotencyKey": "order-1-fulfill",
"queue": "WEBHOOK_DELIVERY",
"payload": { "orderId": 1 }
},
{
"idempotencyKey": "order-2-fulfill",
"queue": "WEBHOOK_DELIVERY",
"payload": { "orderId": 2 }
}
]Idempotency
Submitting the same idempotencyKey again cancels the previous job (best effort) and enqueues a new one. It does not return the old job unchanged.
One-off jobs keep their idempotency row after success until you delete or replace the key.
Schedule next cron occurrence
POST /api/jobs/schedule → 201
Enqueues a single delayed job for the next time the cron expression fires. It does not create a recurring schedule. For recurring work, use YAML cron_jobs.
| Field | Required | Notes |
|---|---|---|
idempotencyKey | Yes | |
queue | Yes | |
schedule | Yes | cron string |
timezone | No | default UTC |
payload | No | object |
[
{
"idempotencyKey": "user-9-digest",
"queue": "REPORT_DIGEST",
"schedule": "0 8 * * *",
"timezone": "Africa/Lagos",
"payload": { "userId": 9 }
}
]Response:
{ "jobs": [{ "id": "user-9-digest" }] }id is the idempotency key, not the pg-boss job id.
After a successful delivery, the idempotency row for schedule jobs is removed. To fire again later, call schedule again (or use static cron).
Signed Node call:
import crypto from 'node:crypto';
const secret = process.env.REQUEST_SIGNING_SECRET;
const body = JSON.stringify([
{
idempotencyKey: 'user-9-digest',
queue: 'REPORT_DIGEST',
schedule: '0 8 * * *',
timezone: 'Africa/Lagos',
payload: { userId: 9 },
},
]);
const t = Math.floor(Date.now() / 1000);
const v1 = crypto
.createHmac('sha512', secret)
.update(`${t}.`)
.update(body)
.digest('hex');
const res = await fetch('http://127.0.0.1:9000/api/jobs/schedule', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-bq-queue-request-signature': `t=${t},v1=${v1}`,
},
body,
});
// 201 { jobs: [ { id: 'user-9-digest' } ] }Your handler receives data with the payload plus idempotencyKey (for example { userId: 9, idempotencyKey: 'user-9-digest' }).
Delete / cancel
POST /api/jobs/delete → 200
Body is a single object (not an array):
{ "idempotencyKey": "user-9-digest" }{ "deleted": true }Missing key → 404 { "error": "Job not found", "idempotencyKey": "..." }.
import crypto from 'node:crypto';
const secret = process.env.REQUEST_SIGNING_SECRET;
const body = JSON.stringify({ idempotencyKey: 'user-9-digest' });
const t = Math.floor(Date.now() / 1000);
const v1 = crypto
.createHmac('sha512', secret)
.update(`${t}.`)
.update(body)
.digest('hex');
await fetch('http://127.0.0.1:9000/api/jobs/delete', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-bq-queue-request-signature': `t=${t},v1=${v1}`,
},
body,
});Use this when a user cancels a scheduled publish or you need the key free without waiting for delivery.
Errors
| Status | When |
|---|---|
| 400 | Bad body shape, unknown queue, invalid cron, invalid runAt |
| 401 | Missing/invalid signature |
| 404 | Delete target not found |
| 503 | Boss not ready |
| 500 | Enqueue failure |
Unknown queue responses include an index for the failing array item.
Curl
Signing with shell tools is easy to get wrong. Prefer the Node createHmac example above or Request signing. Once you have BODY and HEADER_VALUE:
curl -s -X POST http://127.0.0.1:9000/api/jobs/one-off \
-H 'Content-Type: application/json' \
-H "x-bq-queue-request-signature: ${HEADER_VALUE}" \
-d "${BODY}"