service_account_callback_domain_not_allowed 403: fix the callback host

The 403 means webhook_url's host is outside the key's allowed callback domains. An exact host or any subdomain of an allowed domain passes.

4 min readSume
All posts

service_account_callback_domain_not_allowed is a 403 returned when a submit uses mode: "webhook" with a webhook_url whose host is outside the callback domains that the service-account key allows. The body names the host that was rejected and the list that would have passed, so the fix is to use a URL on one of those domains or to ask whoever manages the key to add yours.

The matching rule

The check lowercases the host of the URL and accepts it if it equals an allowed domain or ends with a dot plus that domain. Nothing else counts, so look at the cases:

How a webhook_url host is matched against allowed_callback_domains = [example.com] (Sume API source, read 2026-10-05)
webhook_url hostResultReason
example.comAllowedExact match
hooks.example.comAllowedEnds with .example.com
Hooks.Example.COMAllowedHost is lowercased first
notexample.comRefusedNo dot boundary before example.com
example.com.evil.testRefusedDoes not end with the domain
not a urlRefuseddetails.callback_url is invalid_url

When the check does not run

If the request has no webhook_url, there is nothing to validate, and a polling submit is unaffected. If the key has an empty allow list, no domain restriction applies. So a team that cannot change its allow list can still poll GET /v1/jobs/:id/status while it waits. The polling guide is in Jobs and results.

Check the host before you submit

Test your callback URL against the same rule in CI, so a typo never reaches production. The check below mirrors the documented behavior:

from urllib.parse import urlparse

def allowed(url: str, domains: list[str]) -> bool:
    host = (urlparse(url).hostname or "").lower()
    if not host:
        return False
    return any(host == d or host.endswith("." + d) for d in domains)

for u in ["https://hooks.example.com/sume", "https://notexample.com/x", "nope"]:
    print(u, allowed(u, ["example.com"]))

Debugging a refusal

Read details first. If it holds callback_hostname and allowed_callback_domains, the URL parsed and the host is outside the list: compare them by eye, and watch for staging hosts, tunnel hosts and preview URLs, which are the usual cause, since they differ from the production domain. If it holds callback_url: invalid_url, the string did not parse as an absolute URL, so check for a missing scheme, a stray space or an unexpanded template variable.

Do not work around the policy by pointing at a redirect on an allowed domain without the key owner's agreement. The list exists so that finished jobs are only posted to hosts that the owner approved.

Staging and preview hosts

Keep one callback URL per environment in config, and ask for each environment's domain to be allowed ahead of time. A tunnel URL that changes on each run will fail this check every time, so for local work use polling instead of a webhook, and keep webhooks for hosts with a stable name.

Webhook basics still apply

Passing this check only means Sume may deliver there. Your endpoint still has to verify the x-sume-webhook-signature header over the raw body, as the webhooks page describes, and it should refuse to run when the signing secret is empty.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume