Skip to content

Batch operations

Four batch endpoints let you move many records in a single call:

EndpointDoes
POST …/records/batch/create/Create many
POST …/records/batch/update/Update many
POST …/records/batch/upsert/?matching_property=XUpsert many, matched on X
POST …/records/batch/delete/Delete many

Batches also count as one request against your rate limit, which is the main reason to prefer them.

Batches don’t succeed or fail as a unit. They return 207 with a per-item result, so a single bad record doesn’t discard the rest of the work:

{
"data": [
{ "status": 201, "data": { "id": "rec_01J8Z…" } },
{ "status": 422, "error": {
"code": "request_invalid",
"reason": "schema_invalid",
"field": "/records/1/properties/ahoy_email",
"detail": "Not a valid email address."
}}
]
}

Results come back in the order you sent them, so index n in the response corresponds to index n in your request.

  • An empty batch returns request_invalid with reason: "empty_batch".
  • Too many items returns reason: "batch_too_large". Chunk and send again.

A write whose values already match stored state is a success, not a conflict. Ahoy treats a no-change write as idempotent and returns the current record.

That means a retried batch after a network timeout won’t fail on the records that already landed, and a batch containing the same record twice won’t error. Combined with PUT upserts, this makes at-least-once delivery from your side safe to implement.

matching_property must be a readable property, present and non-null in every item. If it matches two or more existing records the item returns conflict with reason: "ambiguous_match", since there’s no single record to update.

Prefer a property you know is unique — an external id from your own system is usually the right choice.