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.

service_account_metadata_required is a 400 that says your key's policy requires attribution headers and one or more are absent. The body tells you exactly which: details.missing lists the field names and details.headers maps each one to the header you should send. A key that has no required fields never sees this error.
The five attribution headers
The policy reads five single-value headers from the request. A policy can require any subset of them:
| Field in details.missing | Header to send |
|---|---|
| source_product | x-sume-source-product |
| source_environment | x-sume-source-environment |
| source_job_id | x-sume-source-job-id |
| source_user_id_hash | x-sume-source-user-id-hash |
| source_workspace_id_hash | x-sume-source-workspace-id-hash |
Why a 400 is correct, and why it is cheap
The check runs before the spend and queue checks and before any reserve, so a refused request costs nothing and creates no job. It is retryable: false, because the same request without the header will always fail. Add the headers and resend, with the same Idempotency-Key if you set one.
Two header values deserve care. The *_hash fields are meant for a hash, not a raw user or workspace id, so hash on your side and never send personal data. And source_user_id_hash has a second job: when a key has a per-end-user daily cap, that header is how Sume tells which user the spend belongs to.
Build the headers from the error
Instead of hard-coding the list, you can read the map from the first error and attach it to later calls. This sketch runs offline:
import json
BODY = '''{"error": {"code": "service_account_metadata_required",
"details": {"missing": ["source_product", "source_job_id"],
"headers": {"source_product": "x-sume-source-product",
"source_job_id": "x-sume-source-job-id"}}}}'''
VALUES = {"source_product": "my-app", "source_job_id": "batch-17"}
d = json.loads(BODY)["error"]["details"]
headers = {h: VALUES[f] for f, h in d["headers"].items() if f in VALUES}
print(headers)Common mistakes
The first is sending the headers only on the submit and not on the retry: the policy runs on each paid submit, so a retry without them fails the same way. The second is sending an empty string, which is treated as absent, because only non-empty values count as present. The policy reads a single value per header. Set each header once with a non-empty value, from one place in your code.
Tests worth writing
Write one test per required header: send the request with it removed and expect this 400, then send it back and expect the call to proceed. Add a test that your wrapper sends the same attribution on a retry. Keep the expected details.headers map in the test, so that a policy change on the key shows up as a clear diff and not as a surprise in production.
Where to put the logic
Add the headers in one client wrapper so every call carries them, including the polling reads, and keep the values stable per job so your own logs can group the work. Do not put secrets in them: they are labels. If you are unsure whether your key is a service-account key at all, the related posts on 403 insufficient_scope show the other places this shows up.
The general error shape is on the Errors and credits page.
Sources
Related posts
More in Developers
- service_account_model_not_allowed 403: the key's model allowlist
The 403 says the model, or the operation, is not on the service-account key's allowlist. details.model or details.operation names the value to add or avoid.
- service_account_policy_unavailable 503: the message says temporary
The message says temporarily unavailable, but the envelope marks this 503 retryable false with contact_support. Retry cautiously and alert on repeats.
- service_account_rate_limited 429: read the minute and hour headers
A service-account key has an optional minute window and hour window. The 429 names the window, and x-sume-service-account headers show what is left.
- Service-account 402: daily, monthly or per-end-user spend cap hit
Three different 402 codes mean three different caps. Read details.cap_usd_micros and current_usd_micros to see which window blocked the request.
Written by Sume