Sume webhook.test has no job_id: keep it out of your job table

The dashboard's Send test posts a signed webhook.test with no job_id. Route on event first, dedupe on job_id second, and no phantom job row appears.

4 min readSume
All posts

The signed webhook.test event that Sume sends from the dashboard or POST /v1/webhooks/test-deliveries has no job_id and no run id. A handler that inserts payload["job_id"] unconditionally will write None or crash. Route on event first, treat webhook.test as a connectivity check that gets a 204, and dedupe real events on job_id.

The docs say the dummy body is not a job or Format run and that the test never replays a real job.

What arrives

Taken from the webhooks page. Job events and run events are different families with different payloads.

Webhook events a job receiver can see (read 2026-10-07)
EventHas job_id?What to do
job.completedYesStore once, fetch result
job.failedYesStore once, read error
job.canceledYesStore once, mark canceled
webhook.testNoVerify signature, answer 204, store nothing
Run events (*.run.terminal)Run id, not job idDifferent receiver or branch

Why order matters

Verify the signature first, on the raw body, for every event including the test; a test that fails verification is a useful signal that your secret is wrong. Then branch. The idempotency rule in the docs is that receivers treat job_id as the key, and that Redeliver does not create a new key: the same terminal event arrives again with a fresh timestamp and signature.

So the dedupe table holds job ids only. Putting test events in it adds a row with an empty key, and a unique constraint on an empty key will then reject a legitimate event later.

A tested handler core

The function below returns an action name; plug it in after your signature check. It uses an in-memory set here, and a unique database column in production.

SEEN = set()


def handle(event):
    kind = event.get("event")
    if kind == "webhook.test":
        return "ack"
    if kind in ("job.completed", "job.failed", "job.canceled"):
        job_id = event.get("job_id")
        if not job_id:
            return "reject"
        if job_id in SEEN:
            return "duplicate"
        SEEN.add(job_id)
        return "store"
    return "ack"


print(handle({"event": "webhook.test"}))
print(handle({"event": "job.completed", "job_id": "job_x"}))
print(handle({"event": "job.completed", "job_id": "job_x"}))
print(handle({"event": "future.thing"}))

Rehearse a failure

Send a test after every deploy of the receiver and fail the deploy if it does not answer 2xx. Use Redeliver, not the test, when you want to replay a real job; the test never will.

The same rule for unknown events

Apply the same discipline to events you do not know. Verify, log the event name, answer 2xx, and store nothing. Sume's run webhooks are a separate family, and future events may appear; a 500 on an unknown event turns a harmless addition into ten retries.

In your monitoring, count test events separately from real ones, so a burst of tests during setup does not look like traffic. A dedicated /hooks/sume path per environment helps: staging tests should never reach the production table, and the secret differs per workspace anyway.

Log the event name and the signature fingerprint for tests, so a misconfigured secret shows up in the first test delivery and not in the first real job.

  • Verify before you parse.
  • Dedupe on job_id only for the three terminal job events.
  • Use Redeliver to replay a real job.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume