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.

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:
| webhook_url host | Result | Reason |
|---|---|---|
| example.com | Allowed | Exact match |
| hooks.example.com | Allowed | Ends with .example.com |
| Hooks.Example.COM | Allowed | Host is lowercased first |
| notexample.com | Refused | No dot boundary before example.com |
| example.com.evil.test | Refused | Does not end with the domain |
| not a url | Refused | details.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
- Sume 429 concurrency cap or queue cap: which service-account limit?
service_account_concurrency_cap_exceeded counts active jobs; service_account_queue_cap_exceeded counts queued ones. Both are 429. Wait for a job to finish.
- service_account_idempotency_required 400: send an Idempotency-Key
Some service-account keys require an Idempotency-Key on every paid submit. The 400 means the header is missing; add a stable key per intent and resend.
- Service-account 403: key revoked, account revoked or disabled
Three 403 codes stop a service-account key before any other check: key_revoked, revoked and disabled. None is retryable; create or request a replacement.
- service_account_metadata_required 400: which source headers to send
The 400 lists missing fields in details.missing and the header for each in details.headers. Five x-sume-source headers exist; send the ones named.
Written by Sume