Ad run returns 400 invalid_attachment: count your 30 files first
A Format run carries at most 30 files, split 30 images, 10 videos, 10 audio. A Python counter for attachments plus media URLs inside input, before you call.

If a Format run for an ad returns 400 invalid_attachment, the usual cause is the media budget. A run carries at most 30 files in total, and within that at most 30 images, 10 videos and 10 audio files. The budget is shared by attachments[] and every HTTPS media URL anywhere inside input, and Sume counts by file type, not by field name. A brief with 12 product photos, 3 clips and a voiceover is inside the budget. A brief with 11 clips is not, even if the total is small.
What counts
The rule from the Format API page is mechanical, and your code can copy it.
- An HTTPS URL counts if its filename ends in a media extension, at any depth in
input. - A product page URL does not count.
- The same URL counts one time, even if it appears twice.
- Media that the agent finds itself during the run is not yours and does not count.
attachments[]takes images only, asinput_imagewith animage_urlor anasset_id, up to 30.
A counter
The function below walks a JSON object, collects HTTPS strings with a media extension, removes duplicates and returns the count by type. The extension lists are an assumption of this sketch, so adjust them to the formats you actually send. The server stays the final judge.
IMG = (".jpg", ".jpeg", ".png", ".webp", ".gif")
VID = (".mp4", ".mov", ".webm")
AUD = (".mp3", ".wav", ".m4a")
def urls(node):
if isinstance(node, str):
if node.startswith("https://"):
yield node
elif isinstance(node, dict):
for v in node.values():
yield from urls(v)
elif isinstance(node, list):
for v in node:
yield from urls(v)
def budget(body):
found = set(urls(body.get("input", {})))
found |= {a["image_url"] for a in body.get("attachments", []) if "image_url" in a}
kinds = {"image": 0, "video": 0, "audio": 0}
for u in found:
path = u.split("?")[0].lower()
if path.endswith(IMG): kinds["image"] += 1
elif path.endswith(VID): kinds["video"] += 1
elif path.endswith(AUD): kinds["audio"] += 1
return kinds, sum(kinds.values())
body = {"input": {"shots": ["https://x.test/a.jpg", "https://x.test/a.jpg", "https://x.test/v.mp4"]}}
print(budget(body))What to cut when you are over
Cut by information, not by count. Drop near-duplicate product angles first, since the second photo of the same view adds nothing. Drop video references that only show motion you can describe in the instruction. Keep one clean image of each distinct thing that must appear: the product, the label, the person, the setting.
If you still need more material, split the work into two runs of the same Format and join the outputs afterwards, or use previous_run_id to continue the first run with a second set of references. A continuation is a new run with its own receipt and its own budget.
| Request holds | Within budget? | Reason |
|---|---|---|
| 12 images, 3 videos, 1 audio | Yes | 16 files, under every ceiling |
| 31 images | No | Over 30 images |
| 8 images, 11 videos | No | Over 10 videos |
| Same photo URL in input and attachments | Counted once | Duplicates count one time |
| Store page URL and 5 photos | 5 files | Page URL has no media extension |
Check at create, not at minute ten
Sume resolves attachments at create time and answers with a 4xx that you can act on, so a bad budget costs you nothing and fails fast. Inside a bulk queue, the same check happens before the queue exists, and one bad item fails the whole create. Run your counter on every item of a batch before you send it, and log the counts next to the idempotency key.
One trap is worth naming. input can hold URLs at any depth, so a nested list of scenes, each with its own image, adds to the same budget as the top-level attachments. A product feed that you pass through whole can carry far more media URLs than the ad needs. Pass only the fields the Format reads, or trim the feed to the shots you want, and the counter will stay under the ceiling without effort.
If the counter says 30 and the server still says 400, read the details in the error. The same code covers other attachment problems, such as an image that cannot be fetched or is not a real image, and the code does not always mean that the budget was exceeded.
Sources
Related posts
More in Developers
- Agency Black Friday: a team-owned Format needs a workspace key
A personal API key cannot run a Format that a team workspace owns. Sume answers 403 workspace_key_required, so issue the key inside the team before launch.
- Get the edited image URL as a named field from an Agent Completion
Attach the photo and an output_schema in one POST /v1/agent/completions call. Sume parses output into your own fields, such as edited_url and changes.
- Agent Completion output_schema shaped like a Clef or Decider answer
Map Clef and Strands Decider answer types (yes/no, choice, score) onto a Sume output_schema with enum and integer bounds, and see what it does not check.
- Agent Completion result: read output.videos, images, audio and files
A completed Agent Completion puts its last text in output.text and generated media in output.images, videos, audio and files, as durable media.sume.com URLs.
Written by Sume