Bulk run 400 with details.index: validate your ad variants first

One bad item makes the whole Sume bulk create return 400 and starts nothing. A Python pre-check for concurrency 1 to 16, 1 to 100 items, and the four item keys.

5 min readSume
All posts

A bulk create is all or nothing at the door. If one entry in items is not valid, POST /v1/formats/{handle}/{slug}/bulk-runs answers 400 invalid_request with details.index naming the bad item, Sume dispatches nothing, and no queue exists. That is good for your wallet and bad for a 100-ad batch you built over an hour, so check the list in your own code before you send it. The Bulk runs page gives the rules.

The rules the server applies

These are the checks that fail the create. Your pre-check should reproduce all of them.

Bulk create validation rules (Sume docs, read 2026-10-05)
FieldRuleFailure
concurrencyRequired integer from 1 to 16400 invalid_request
itemsRequired array of 1 to 100 entries400 invalid_request
each itemAn object that names at least one of instruction, input, previous_run_id, attachments400 invalid_request with details.index
top levelUnknown fields are rejected400
attachmentsResolved before the queue existsinvalid_attachment, attachment_not_found, attachment_too_large, attachment_fetch_failed

A pre-check you can run

The function below returns a list of problems and does not call the network. It also splits a long list into chunks of 100, because the queue takes at most 100 items. It does not replace the server, which also checks media types and sizes.

KEYS = ("instruction", "input", "previous_run_id", "attachments")

def problems(items, concurrency):
    out = []
    if not isinstance(concurrency, int) or not 1 <= concurrency <= 16:
        out.append("concurrency must be an integer from 1 to 16")
    if not isinstance(items, list) or not 1 <= len(items) <= 100:
        out.append("items must hold 1 to 100 entries")
        return out
    for i, item in enumerate(items):
        if not isinstance(item, dict):
            out.append(f"items[{i}] is not an object")
        elif not any(item.get(k) for k in KEYS):
            out.append(f"items[{i}] names none of {', '.join(KEYS)}")
    return out

def chunks(items, size=100):
    for start in range(0, len(items), size):
        yield items[start:start + size]

ads = [{"instruction": f"variant {n}"} for n in range(120)]
for part in chunks(ads):
    print(len(part), problems(part, 4))

Why the check pays

Ad variants are generated from a sheet, and a sheet has empty rows. An empty row becomes {}, which names nothing, and the create fails on its index. Skipping blank rows in code is a one-line fix. The pre-check also catches an items list of 101 when you forgot to chunk.

Remember the idempotency scope. A key is scoped to one Format, and a replay of the same key with the same payload returns the old queue with 202. If your create fails with 400, no queue exists for that key, so fix the list and resend it. Use a new key if you want to be certain that nothing from the failed attempt is replayed.

Attachment errors come before the queue too

Items can carry attachments of up to 30 images each, and Sume resolves them at create, before it makes the queue. A dead image URL in item 57 fails the whole create with attachment_fetch_failed, or attachment_not_found for an asset id. If your variants share a product photo, test that one URL once, with a plain GET, before the batch. If the variants use many different images, expect to fix a few and resend.

Log the index and the message from the 400 body, and map the index back to a row in your sheet, not to a position in a list you have since reordered. The index is zero-based and is the position in the items array you sent. If you removed blank rows before the call, keep a list of the original row numbers, so a message about items[12] points to the row a person can fix.

Last, keep the concurrency choice simple. The window is 1 to 16 and a higher number does not bypass the limits that each child still meets: wallet, workspace generation concurrency and spend caps all apply per run. Start at 4 for a first batch, watch the failures, and raise it if the items are cheap and the failures are none.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume