Bulk Format run 403 workspace_key_required: an agency's fix
A personal key on a team workspace Format returns 403 workspace_key_required, and a service-account key fails too. Which key to mint for Sume bulk runs.

Your key was created outside the team workspace that owns the Format. The bulk runs docs list workspace_key_required as 403 with the instruction to use a key created in details.workspace_id. This hits agencies first, because they keep client Formats in a shared team workspace while engineers hold personal keys.
Three keys that fail, one that works
| Key | Result | Fix |
|---|---|---|
| Personal key, team Format | 403 workspace_key_required | Create a key inside the workspace in details.workspace_id |
| Service-account key | 403 insufficient_scope, reason service_account_format_runs_unsupported | Use a user API key |
| Older key without scopes | 403 insufficient_scope | Mint a new key; scopes cannot be patched |
| User key made in the team workspace, with formats:write and formats:read | 202 | Works |
Scopes
- formats:write creates the queue (and single runs and cancels).
- formats:read polls GET /v1/format-run-queues/{id}. A key with write only can create but not poll.
- Keys created before the Format API-call trigger shipped do not carry these scopes.
Why a 404 can be the same problem
A Format that is archived, outside your key's workspace, or owned by a team you are not in comes back as format_not_found, not 403. If you have the right scopes and still see 404, check the workspace the key was made in before you check the handle spelling. The Calling a Format page covers the per-item contract.
Read the error code in code
import json, os, urllib.error, urllib.request
req = urllib.request.Request(
"https://api.sume.com/v1/formats/your-handle/product-promo/bulk-runs",
data=json.dumps({"concurrency": 1, "items": [{"instruction": "test"}]}).encode(),
method="POST",
headers={
"Authorization": "Bearer " + os.environ["SUME_API_KEY"],
"Content-Type": "application/json",
"Idempotency-Key": "scope-check-1",
},
)
try:
urllib.request.urlopen(req)
except urllib.error.HTTPError as e:
err = json.load(e).get("error", {})
print(e.code, err.get("code"), err.get("details"))Sources
Related posts
More in Formats
- Retrying a bulk Format queue: same Idempotency-Key, same queue
Resend a Sume bulk queue with the same Idempotency-Key and payload and you get 202 and the original queue. Change one item and you get 409. How to retry safely.
- Bulk run items ignore on_active_run skip and reject
A Format bulk queue runs every item with on_active_run allow, so skip or reject on an item will not serialize it. Set concurrency to control parallel runs.
- Revise 20 finished videos in one Sume bulk queue with previous_run_id
Each bulk item can carry previous_run_id, so one queue re-edits twenty finished runs. A preflight for thread_id, the repeated schema, and the cost per item.
- Bulk virtual try-on videos for a catalog: 100 SKUs per queue
Queue up to 100 try-on runs in one POST: concurrency 1 to 16, one garment image per item, a spend cap each, and how to read the failures afterwards.
Written by Sume