Pub/Sub push ack codes and a Sume webhook relay with a signature check

Pub/Sub treats only 102, 200, 201, 202 and 204 as acks. Verify Sume's sume-v1 signature at the edge, publish, return 2xx, and let Pub/Sub retry the workers.

5 min readSume
All posts

A Pub/Sub push subscription acknowledges a message only when your endpoint returns 102, 200, 201, 202 or 204; any other status is a negative acknowledgment and the message is sent again. For Sume webhooks, verify the signature at an edge endpoint, publish the event, and return a 2xx quickly, then let the push subscription drive your workers.

Two hops, two retry systems

Sume posts a signed job.completed, job.failed or job.canceled event to your callback_url. It retries up to 10 attempts total with a fixed 30-second delay and a 10-second timeout per attempt. Google's push docs say that when a subscriber sends too many negative acknowledgments, Pub/Sub applies push backoff between 100 milliseconds and 60 seconds. Keep the edge hop tiny so the 10-second budget is never at risk.

Ack and retry rules on each hop (read 2026-10-05)
HopAckRetry behavior
Sume to your edgeAny 2xx10 attempts, 30 s apart, 10 s timeout each
Pub/Sub to workers102, 200, 201, 202, 204Other codes are resent with push backoff
Authenticated pushJWT in the authorization headerVerify on the worker
Dedup keyjob_idProcess each job_id once

The verifier

Sume signs <timestamp>.<raw_body> with HMAC SHA 256 and sends sume-v1=<hex>. During a rotation the header carries one entry per live secret, so accept any match. The function refuses an empty secret and an old timestamp.

import hmac, hashlib, time

def verify(secret, timestamp, header, raw_body, tolerance=300):
    if not secret:
        raise ValueError("refusing to verify with an empty secret")
    if abs(time.time() - int(timestamp)) > tolerance:
        return False
    mac = hmac.new(secret.encode(), f"{timestamp}.{raw_body}".encode(),
                   hashlib.sha256).hexdigest()
    expected = f"sume-v1={mac}"
    ok = False
    for entry in header.split(","):
        ok |= hmac.compare_digest(entry.strip(), expected)
    return ok

ts = str(int(time.time()))
body = '{"event":"job.completed","job_id":"job_demo"}'
sig = "sume-v1=" + hmac.new(b"s3cret", f"{ts}.{body}".encode(),
                            hashlib.sha256).hexdigest()
print(verify("s3cret", ts, sig, body), verify("s3cret", ts, "sume-v1=bad", body))

What the edge does

After the check passes, publish the raw body with job_id as an attribute, then return 204. A failed check returns 401 and is not published. On the worker side, your push handler returns 204 only after the result has been stored, because any other code makes Pub/Sub resend. Since either hop can deliver twice, store job_id with a unique constraint and ignore repeats.

Rotation and testing

Reject deliveries with a timestamp outside five minutes, which is the default replay window in the Sume docs. A short window blocks replays without harming normal delivery.

  • Use Send test on the dashboard to check the edge without a real job.
  • Rotate the signing secret in a quiet window, and accept both entries in the header during the change.
  • Compare the secret fingerprint header with the one in the dashboard if a check fails.
  • Log the job_id and the verdict, never the secret.

Fallback

After ten refused attempts Sume stops, but the job is still finished. Keep a poll on the job status for events that never arrive, and redeliver with POST /v1/jobs/{job_id}/webhook/redeliver when you need a fresh signed copy. See Webhooks for headers and rotation.

Sources

Related posts

More in Integrations

All Integrations posts

Written by Sume