Skip to Content
Receiving jobs

Receiving jobs

When a job runs, queue-service calls your queue endpoint with a signed JSON body. Your service must verify the signature with the queue signature_secret, then process data.

Request shape

POST /hooks/deliver HTTP/1.1 Content-Type: application/json x-bq-queue-request-signature: t=1710000000,v1=...
{ "timestamp": 1710000000, "data": { "invoiceId": 42, "action": "send" } }

The content of data depends on how the job was created:

SourceTypical data
POST /api/jobs/one-offYour payload as sent (for example { invoiceId: 42 })
POST /api/jobs/scheduleYour payload plus idempotencyKey
YAML cron_jobsThe cron payload from config (for example { type: 'daily_digest' })

Timeout on the worker side is 2 minutes. Return 2xx only when the work succeeded. Anything else is failed and retried per queue config (retryLimit, retryDelay, retryBackoff).

Express handler

import express from 'express'; import { verifySignature } from './verifySignature.js'; // see Request signing const app = express(); const QUEUE_SECRET = process.env.WEBHOOK_SIGNING_SECRET; const HEADER = 'x-bq-queue-request-signature'; app.post( '/hooks/deliver', express.raw({ type: 'application/json' }), async (req, res) => { const raw = req.body.toString('utf8'); const header = req.headers[HEADER]; if (!verifySignature(raw, header, QUEUE_SECRET)) { return res.status(401).json({ error: 'Invalid signature' }); } const parsed = JSON.parse(raw); const payload = parsed.data ?? {}; // do the work await deliverWebhook(payload); return res.status(200).json({ ok: true }); }, );

Use express.raw (or equivalent) so verification sees the same bytes the worker signed. Parsing JSON first and re-stringifying will fail verification.

Routing by payload

One endpoint can serve both API jobs and static crons if you branch on fields you control:

const { data } = JSON.parse(raw); if (data.type === 'daily_digest') { await runDailyDigest(); } else if (data.action === 'publish') { await publishPost(data.postId); } else { await deliverWebhook(data); } return res.status(200).json({ ok: true });

Return 5xx (or any non-2xx) if the work failed and should retry. Return 2xx only when it is safe not to run again.

What to put behind the URL

  • Prefer private network URLs when both sides live in the same VPC/cluster.
  • If the handler is reachable from the public internet, keep TLS at the edge and treat signature_secret as the only proof the caller is queue-service.
  • Rotate queue secrets by updating YAML/env and redeploying both sides together.

After success

  • Optional wait_interval_after_success_seconds on the queue delays the next job from that queue.
  • Schedule-API jobs clean up their idempotency row after success.
  • One-off idempotency rows remain until delete or replace. Call delete if you want the key reusable without canceling a live job first.
Last updated on