Quick answer: A bulk endpoint accepts many items in one request and can succeed on some while rejecting others, so the top-level status code does not tell you what happened. First pin down whether the endpoint is atomic (all items commit or all roll back) or non-atomic (each item is independent), then test the per-item result array across all-success, all-fail, and mixed cases, match every result back to its request item by identifier, and confirm the client lets the user retry only the failed entries without replaying the ones that already landed. Asserting on the single outer status is the mistake that hides a half-applied batch.
First pin down the contract
The two designs behave differently under failure and the tests follow from which one you have. An atomic endpoint treats the whole batch as one transaction: if any item is invalid it returns a single 4xx and nothing is written, so there is no partial state to reconcile. A non-atomic endpoint processes each item independently and returns a result per entry, so a batch can leave some resources created and others rejected. The documentation should state which model applies; if it does not, send a mixed batch and observe whether the valid items persist before you write the suite. Do not assume the presence of a bulk endpoint means a transaction.
The response shapes
The explicit form is 207 Multi-Status, whose body lists each entry with its own status, identifier, and error detail. Other APIs return 200 with a result array where every item carries a success flag, or 202 Accepted and process the batch asynchronously with a status endpoint to poll. In every non-atomic shape the meaningful result is the per-entry outcome, not the outer code. A 207 can contain both 201 and 409 entries, and a 200 can carry failed items, so a client that branches on the outer status alone will misread the batch.
The test matrix
| Case | Request | Expected per-item result |
|---|---|---|
| All valid | Clean items | Every entry 2xx, all side effects present |
| All invalid | Bad items | Every entry 4xx, nothing written |
| Mixed | Valid and bad items | Success and failure entries both returned |
| Empty or single | Zero or one item | Handled, no crash or false result |
| Duplicate id | Same identifier twice | Conflict flagged, not silently merged |
| Over size limit | More than the cap | Rejected or bounded, never truncated silently |
| Partial then retry | Re-send failed items | Failed items fixed, landed items not duplicated |
Matching results and retrying
Match each result to its request by the client-supplied identifier or the resource id, not by array position, because the server may reorder or drop entries. For the mixed case, assert the successful items exist and the rejected ones have no side effect, then check the UI renders the two groups separately so the user can act on the failures. A retry should send only the failed entries and rely on idempotency so the already-created items are not duplicated; re-sending the entire batch risks a second set of resources. In a desktop API workspace you can run the bulk request and the follow-up retry as an ordered scenario and inspect each sub-result. Powerduck supports this flow, keeping the per-item sequence deterministic rather than eyeballing a large response.
Honest limits
These tests prove the client handles per-item results and retries correctly, but they do not prove the real server's transaction boundaries, partial-commit behavior, database constraints, or how it processes a large batch under load. A local mock cannot reproduce an async worker that fails after accepting the job, race conditions between concurrent batches, or the latency of a queue, so confirm the production path with a genuine mixed batch and verify which items actually persisted. A passing run says nothing about whether the server stays consistent when a worker crashes mid-batch, and importing a schema does not deploy a backend.
Frequently asked questions
Does 207 mean the request failed? No. It means the response carries multiple independent statuses; read each entry rather than treating the whole call as an error.
How do I know if the endpoint is atomic? Send a mixed batch and check whether the valid items persist. If they do, it is non-atomic; if nothing is written, it is atomic.
What should I do after a partial failure? Retry only the failed items by id, confirm the landed items are not re-sent, and verify the retry does not create duplicates.
Can I trust result order? No. Match by identifier, since the server may process or return items in a different order.
The bottom line
Decide atomicity first, then drive the per-item result array across all-success, all-fail, mixed, duplicate, and retry cases, matching entries by identifier and asserting both the response and the side effects. Confirm the real server's partial-commit behavior separately. For more on this approach, see the API testing guide hub.
