Node 26.10 SQLite binds undefined as NULL: a Sume webhook job table
Node 26.10 binds undefined as NULL in node:sqlite. A Sume job-webhook handler can still be explicit with null and dedupe on job_id without a driver.

If you store Sume webhook events with node:sqlite, write null yourself and do not lean on undefined. The Node.js 26.10.0 release notes say SQLite now binds undefined as NULL (Node.js 26.10.0 release notes, read 2026-10-04). On Node 22.14 the same call throws Provided value cannot be bound to SQLite parameter, which I confirmed by running the script below. A handler that works on your laptop on one Node line can therefore fail on a server running another.
A job webhook is a natural place to hit this. A job.completed event carries payload.artifacts, and a job.failed event carries an error object instead, so one of the two optional columns is always missing. Optional chaining then yields undefined, which is exactly the value whose binding changed.
What the job events contain
Sume sends terminal job events only: job.completed, job.failed and job.canceled. Each body has event, request_id, job_id, status and either a payload with artifacts or an error object (Webhooks). The docs tell you to use job_id as the idempotency key on your side, since a delivery can repeat, and to return any 2xx after durably storing the event.
That maps to a table with job_id as the primary key and an insert that does nothing on conflict. The return value of run() then tells you whether this delivery was new.
| Webhook field | Column | When missing |
|---|---|---|
| job_id | job_id (primary key) | Never; reject the delivery |
| event | event | Never |
| payload.artifacts[0].url | artifact_url | null on failed and canceled |
| error.code | error_code | null on completed |
A handler body that runs on Node 22 and 26
The function below parses the raw body after your signature check, flattens the optional fields with ?? null, and returns true only for a first delivery. It uses an in-memory database so you can run it as a test; swap in a file path for real use.
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync(":memory:");
db.exec(`CREATE TABLE IF NOT EXISTS sume_jobs (
job_id TEXT PRIMARY KEY,
event TEXT NOT NULL,
artifact_url TEXT,
error_code TEXT,
received_at INTEGER NOT NULL
)`);
const upsert = db.prepare(`INSERT INTO sume_jobs (job_id, event, artifact_url, error_code, received_at)
VALUES (?, ?, ?, ?, ?) ON CONFLICT(job_id) DO NOTHING`);
export function record(body) {
const e = JSON.parse(body);
const art = e.payload?.artifacts?.[0];
const r = upsert.run(e.job_id, e.event, art?.url ?? null, e.error?.code ?? null, Date.now());
return r.changes === 1;
}
console.log(record('{"event":"job.completed","job_id":"job_1","payload":{"artifacts":[{"url":"https://media.sume.com/artifacts/a"}]}}'));
console.log(record('{"event":"job.completed","job_id":"job_1"}'));
console.log(db.prepare("SELECT * FROM sume_jobs").all());Operational notes
- Verify the signature on the raw body before you parse or insert anything. The SDK exports
verifyWebhookfor that. - Reply 2xx only after the insert commits. A non-2xx reply is retried, up to 10 attempts at a fixed 30-second spacing.
- Keep polling
GET /v1/jobs/{id}as the backstop for a delivery that never arrives. - Pin the Node version in your container so the binding behavior you tested is the one you run.
Sources
Related posts
More in Developers
- Nuxt server route for Sume webhooks: readRawBody, then verify
A Nuxt 3 or 4 server route at server/api/sume.post.ts that reads the raw body with readRawBody, checks sume-v1 with node:crypto, and answers 204 fast.
- Oban worker that polls a Sume job: snooze instead of sleeping
Submit a Sume job once, then let an Oban worker check status, return {:snooze, seconds} while it runs and finish on terminal. Elixir with Req, no sleep loop.
- One Gemini billing cap pauses every linked project: guard the batch
Google pauses all projects on a billing account at the tier cap. Add a balance check and idempotent submits so one Omni batch cannot stall the rest.
- One-hour Sume OAuth token in an OpenAI MCP authorization field
OpenAI does not store the MCP authorization token and wants it on every request. Sume's token lasts one hour with no refresh, so track expiry yourself.
Written by Sume