Batch

Developer·4 minutes to read

Verified against docs/superpowers/specs/2026-09-15-p4-a-typed-http-api-openapi-webhooks.md §4.1, §4.3, §4.4, §6; re-verified against the code at close-out.

What it does

One request carries up to fifty operations, runs them in order inside one transaction, and either commits all of them or commits none (F-DEV-5 — the typed HTTP contract's batch form).

Authentication

An organisation API key of scope write or wider. One operation needs more than that: an invitation.create item needs admin, and the check is per item, so a write key sending a batch that contains one is refused on that item and the whole batch rolls back. See Authentication and keys.

Request

POST /batch

<!-- generated:begin POST /batch --> <!-- generated:end POST /batch -->

Eleven operations may appear in a batch, and they are the create, update and delete verbs of the four resources a script builds with, plus the two that start work:

bot.create · bot.update · bot.delete · routine.create · routine.update · routine.delete · memory.create · memory.update · memory.delete · invitation.create · run.create

Keys, webhook endpoints and organisations are not among them: a credential is never created in a batch.

Each item may carry a ref of up to 40 characters. A later item names an earlier item's result with $ref:<ref> wherever an id is expected — so a bot and its routine are created in one call, before the bot's id exists anywhere outside the request. A $ref to a later item, or to one that failed, is invalid_request for the item that used it.

The whole batch is one idempotency row, so a retry with the same Idempotency-Key and the same body replays the first answer rather than running fifty operations twice.

curl -sS -X POST "$BOTSEON_API_ORIGIN/api/v1/batch" \
  -H "authorization: Bearer $BOTSEON_API_KEY" \
  -H "idempotency-key: 8d9e0f1a-2b3c-4d5e-8f9a-0b1c2d3e4f5a" \
  -H "content-type: application/json" \
  -d '{"operations":[{"op":"bot.create","ref":"triage","input":{"from":"template","templateId":"inbox-triage","name":"Inbox Triage"}},{"op":"routine.create","ref":"morning","input":{"botId":"$ref:triage","name":"Morning triage","instruction":"Sort the overnight mail.","triggers":[{"kind":"schedule","cron":"0 7 * * 1-5","timezone":"Europe/Copenhagen"}]}}]}'
const res = await fetch(`${process.env.BOTSEON_API_ORIGIN}/api/v1/batch`, {
  method: 'POST',
  headers: {
    authorization: `Bearer ${process.env.BOTSEON_API_KEY}`,
    'idempotency-key': crypto.randomUUID(),
    'content-type': 'application/json',
  },
  body: JSON.stringify({
    operations: [
      {
        op: 'bot.create',
        ref: 'triage',
        input: { from: 'template', templateId: 'inbox-triage', name: 'Inbox Triage' },
      },
    ],
  }),
});
const result = (await res.json()) as {
  ok: boolean;
  results: Array<{ ok: boolean; ref: string | null; id?: string | null }>;
};
if (!result.ok) throw new Error('batch rolled back');
for (const item of result.results) console.log(item.ref, item.id);

Response

A batch that succeeds answers 200 with ok: true and one result per operation, in order:

{
  "ok": true,
  "results": [
    { "ok": true, "ref": "triage", "id": "0f3a2b71-6c4d-4a1e-9b2c-7d5e8f0a1b2c" },
    { "ok": true, "ref": "morning", "id": "5d6e7f80-1a2b-4c3d-9e0f-1a2b3c4d5e6f" }
  ]
}

A batch that fails answers 422 with ok: false and the same list — every operation is still evaluated, so one round trip tells you every error rather than the first one:

{
  "ok": false,
  "results": [
    { "ok": true, "ref": "triage", "id": "0f3a2b71-6c4d-4a1e-9b2c-7d5e8f0a1b2c" },
    {
      "ok": false,
      "ref": "morning",
      "error": {
        "code": "invalid_request",
        "message": "The request was not accepted.",
        "details": [{ "path": "triggers.0.cron", "message": "Invalid cron expression" }]
      }
    }
  ]
}

The id on a successful item in a failed batch is the id that operation would have had. Nothing was committed.

Errors

StatusCodeWhenWhat to do
422One or more items failed. The status is 422 and the body is the result list above; there is no envelope, because the failure is per item.Read results, fix the items that failed, and send the batch again with a new Idempotency-Key.
400invalid_requestMore than fifty operations, an empty list, an unknown op, or no Idempotency-Key. This is refused at the schema, before any transaction opens.Split the work into batches of fifty or fewer.
401unauthenticatedThe credential is missing, refused, or below write.Use a write key, and an admin key when the batch invites anybody.
413payload_too_largeThe body is over 1 MB.Send fewer operations, or shorter instructions.
402payment_requiredThe organisation needs a trial or a payment.Settle the account, then retry.
429rate_limitedA bucket is full.Wait for Retry-After.

Rate limits

A batch is one request against the two one-minute buckets, 300 per key and 1 000 per organisation, however many operations it carries — which is the reason to prefer it over fifty separate calls. The operations inside it still meet their own product ceilings: fifty operations is the batch's limit, and a run.create inside a batch counts against the organisation's run starts for the hour exactly as a direct one does. A full bucket answers rate_limited with Retry-After: 60; wait that many seconds and retry. Failed authentication is counted separately, against the caller's address, so a working key's allowance is never spent by somebody else's bad credential. See Envelopes, errors, idempotency and paging.

Last verified against build c0f77aa.