Sanity webhook to Sume: reuse its idempotency-key header

Sanity delivers at least once with an idempotency-key header. Prefix it and pass it as the Sume Idempotency-Key so a repeated delivery is one job.

4 min readSume
All posts

When a Sanity document is published and you want a Sume cover image, take the idempotency-key header Sanity attaches to every delivery and send it, with a prefix, as the Sume Idempotency-Key. Sanity delivers at least once, so the same publish can reach you more than once; the same key makes every copy resolve to one Sume job.

Sanity's webhooks are configured with a GROQ filter and projection, which is the right place to decide which documents trigger generation and to send only the fields the prompt needs.

What Sanity documents

The webhooks page describes a 30-second request timeout, two retries spaced 30 seconds apart, and retry on 429 and 5xx while other 4xx codes are treated as undeliverable. It also says an idempotency-key header is included so the receiver can dedupe, and that delivery is at-least-once. Signing is described as following the same standard as Stripe, through Sanity's webhook toolkit; use that toolkit for verification rather than writing your own.

The 30-second window is longer than the Sume synchronous wait, which is clamped to 30 seconds, but it is not a reason to block. Submit with mode: "async" and answer 200 immediately. A 4xx from your handler will not be retried by Sanity, so return 5xx only when you want another attempt.

Sanity delivery rules and the Sume submit (read 2026-10-03)
Sanity behaviorValueConsequence
Timeout30 secondsStill submit async and return
Retries2, 30 seconds apartWindow is about a minute
Retried statuses429 and 5xxReturn 503 on a Sume 429
Other 4xxUndeliverableReturn 200 for a bad prompt and log it
Dedupe headeridempotency-keyPrefix and forward to Sume
GuaranteeAt least onceNever assume a single delivery

The submit step

This runs after you have verified the delivery with Sanity's toolkit. It takes the verified header and the projected document title and returns the status code your endpoint should answer with.

import os, requests

def submit_cover(idempotency_key: str, title: str) -> int:
    if not idempotency_key:
        return 400
    try:
        r = requests.post(
            "https://api.sume.com/v1/images",
            headers={
                "Authorization": f"Bearer {os.environ['SUME_API_KEY']}",
                "Idempotency-Key": f"sanity-{idempotency_key}",
            },
            json={"model": "sume/auto", "mode": "async",
                  "prompt": f"Editorial cover illustration for: {title}"},
            timeout=20,
        )
    except requests.RequestException:
        return 503
    if r.status_code in (202, 200):
        return 200
    return 503 if r.status_code in (429, 503) or r.status_code >= 500 else 200

Where the key can mislead

Sanity's header identifies a delivery, and its retries reuse it, but a second publish of the same document is a new event with a new key. If you want exactly one cover per document no matter how many times it is edited, key on the document id instead and accept that later edits will not regenerate. Pick which behavior you mean before wiring the filter, because Sume will answer a changed prompt under a reused key with 409 idempotency_conflict.

Sources

Related posts

More in Integrations

All Integrations posts

Written by Sume