Sume webhook URL localhost returns 400: test with a tunnel

Sume refuses localhost, private ranges, and plain HTTP webhook URLs with a 400 at create. Use a public HTTPS tunnel and verify the signature.

4 min readSume
All posts

If your Sume webhook_url points at localhost, a private address, or plain HTTP, the create call returns 400. Expose your local handler through a public HTTPS tunnel, put that URL in the request, and verify the signature in the handler.

Sume rejects those URLs at create time rather than failing quietly during delivery, so you find the mistake in seconds. The rule is on the Run webhooks page: the URL must be public HTTPS and at most 2048 characters. Redirects are not followed, so the tunnel URL must answer directly.

That fast failure is a kindness. A webhook that silently never delivers is a long afternoon, while a 400 at create tells you the URL class is wrong before any money moves.

How other vendors handle local development

Stripe's webhook guide, read 2026-10-10, tells you to use a tunnelling tool such as ngrok for a temporary public HTTPS URL, or to forward events with the Stripe CLI, which listens and forwards to a local port. Stripe's CLI path works because Stripe controls the sender and the forwarder.

Sume does not ship a forwarding listener for webhooks, so a tunnel you run yourself is the route. A hosted tunnel, a reverse proxy on a small server, or a preview deployment all satisfy the public HTTPS requirement.

Local webhook testing, Stripe page read 2026-10-10 and Sume docs read 2026-10-10
QuestionStripeSume
Localhost URL accepted at registrationRegistered endpoints must be public HTTPSNo, 400 at create
Forward events to a local portstripe listen --forward-toNo listener, use your own tunnel
Signature headerStripe-Signaturex-sume-webhook-signature
Default replay tolerance5 minutes in libraries5 minutes

The test loop

Start your handler and the tunnel. Send a test delivery with POST /v1/webhooks/test-deliveries, which sends a webhook.test event to a URL you choose, so you can check signature handling without paying for a generation. Then run a real, cheap Format run with a low generation_spend_cap_usd and watch the terminal delivery arrive.

If a delivery never arrives, the run receipt carries webhook_delivery.status. The values are not_armed, pending, retrying, delivered, failed, and exhausted. not_armed means no webhook was attached to that run.

A cheap run still costs something, so keep the cap small on test runs. generation_spend_cap_usd must be above zero and at most 500, and a run that would exceed it ends failed with format_run_failed rather than spending past the limit.

Verify before you parse

Compute HMAC-SHA256 over the string timestamp, a dot, and the raw request body, and compare it to the sume-v1 value in x-sume-webhook-signature. Reject anything older than five minutes and refuse to run at all when the secret is empty.

Your framework must not re-serialize the body before you hash it. In Node, read the raw bytes; in Python frameworks, read the request body as bytes. Dedupe on request_id, which equals the run id, because a delivery can arrive more than once.

Return a 2xx status once the event is stored. Delivery has a 10 second timeout per attempt and up to 10 attempts, so a handler that takes 12 seconds will look like a failure and trigger retries even though your code finished.

import hashlib, hmac, os, time

def verify(raw: bytes, ts: str, header: str) -> bool:
    secret = os.environ.get("SUME_COM_WEBHOOK_SIGNING_SECRET", "")
    if not secret:
        raise RuntimeError("signing secret is empty")
    if abs(time.time() - int(ts)) > 300:
        return False
    mac = hmac.new(secret.encode(), ts.encode() + b"." + raw, hashlib.sha256)
    return hmac.compare_digest("sume-v1=" + mac.hexdigest(), header)

Before you go to production

Swap the tunnel for your real HTTPS host, fetch the signing secret from the dashboard Webhooks tab or GET /v1/webhooks/signing-secret, and return 2xx quickly. Do slow work after you have saved the event, as Stripe also advises.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume