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.

5 min readSume
All posts

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, as input_image with an image_url or an asset_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.

Media budget outcomes for a Format run (Sume docs, read 2026-10-05)
Request holdsWithin budget?Reason
12 images, 3 videos, 1 audioYes16 files, under every ceiling
31 imagesNoOver 30 images
8 images, 11 videosNoOver 10 videos
Same photo URL in input and attachmentsCounted onceDuplicates count one time
Store page URL and 5 photos5 filesPage 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

All Developers posts

Written by Sume