Bulk create and update
Load or change up to 25 properties in one call. The whole batch is validated before anything is written; then each property is created or updated on its own, and the response tells you, item by item, what happened.
Headers
| Field | Type | Required | Description |
|---|---|---|---|
Authorization
|
string | Required | Bearer <your API key>. |
Content-Type
|
string | Required | application/json. |
Idempotency-Key
|
string | POST only | 8 to 128 characters, unique per batch attempt. The whole batch response is stored against it. |
Request body
Body
| Field | Type | Required | Description |
|---|---|---|---|
properties
|
array | Required | 1 to 25 entries. |
properties[]
|
object | Required | On POST: a create body. On PUT: an update body plus the reference below. |
properties[].doorloom_id
|
integer | PUT, one of the two | Which property this entry changes. |
properties[].external_id
|
string | PUT, one of the two | Which property this entry changes, by your id. In a bulk update this field only identifies the property; it never re-maps it. Use the single PUT /properties/{id} for that. |
How a batch is processed
- The whole payload is validated first. A structural error anywhere —
a missing
namein item 7, a duplicatedexternal_idbetween items 2 and 9 — fails the request with422 VALIDATION_FAILEDand nothing is written. Theerrorskeys are prefixed with the item index:properties.7.name. - Then each item runs on its own, in order, in its own transaction. A refusal
that can only be known at write time — an
external_idalready mapped, a property no longer in your coverage — fails that item and that item alone. The others go through. - The response reports every item, successes and failures, with a summary.
curl -X POST https://doorloom.com/api/integrations/v1/properties/bulk \
-H "Authorization: Bearer $DOORLOOM_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 9a7e2d0c-5b41-4f6e-8c3d-1e2f3a4b5c6d" \
-d '{
"properties": [
{ "external_id": "VILLA-001", "name": "Amber Villa", "description": "…", "address": { "…": "…" }, "space": { "bedrooms": 3, "washrooms": 3 }, "occupancy": { "max_guests": 8 }, "pricing": { "weekday": { "base_price": 12000 }, "weekend": { "base_price": 15000 } } },
{ "external_id": "VILLA-002", "name": "Coral Villa", "description": "…", "address": { "…": "…" }, "space": { "bedrooms": 2, "washrooms": 2 }, "occupancy": { "max_guests": 6 }, "pricing": { "weekday": { "base_price": 9000 }, "weekend": { "base_price": 11000 } } }
]
}'
Response
| Field | Type | Description |
|---|---|---|
201
|
all created | Bulk create, every item succeeded. |
200
|
completed | Bulk update (always), or a bulk create in which at least one item failed. success is still true: the request was processed. Read data.summary and data.results[]. |
422
|
nothing written | The payload failed validation as a whole. |
{
"success": true,
"message": "Some properties were not created",
"data": {
"summary": {
"requested": 2,
"succeeded": 1,
"failed": 1
},
"results": [
{
"index": 0,
"external_id": "VILLA-001",
"status": "created",
"property": {
"doorloom_id": 902,
"external_id": "VILLA-001",
"coverage": "active",
"name": "Amber Villa",
"…": "…"
}
},
{
"index": 1,
"external_id": "VILLA-002",
"status": "failed",
"error": {
"code": "PROPERTY_EXTERNAL_ID_EXISTS",
"message": "A property with this external_id already exists for your integration.",
"errors": {
"external_id": "VILLA-002",
"doorloom_id": 877
}
}
}
]
}
}
Response fields
| Field | Type | Description |
|---|---|---|
summary.requested
|
integer | Items in the batch. |
summary.succeeded
|
integer | Items created (or updated). |
summary.failed
|
integer | Items refused. |
results[].index
|
integer | Position in your properties[], from 0. |
results[].external_id
|
string|null | On a create: the id you sent for that item. |
results[].doorloom_id
|
integer|null | On an update: the property the entry resolved to. |
results[].status
|
string | created, updated or failed. |
results[].property
|
object | On success: the property block, as GET /properties returns it. |
results[].error
|
object | On failure: code, message and, where the single endpoint would carry one, errors — the same shapes as the single-endpoint responses. |
Idempotency and retries
A bulk create is stored against its Idempotency-Key as a whole:
resending the identical batch under the same key replays the identical response, including the
items that failed, and creates nothing. To retry only the failed items after fixing them, send a
new batch containing just those items under a new key — the items that succeeded
would otherwise be refused with PROPERTY_EXTERNAL_ID_EXISTS, which is the safety net
if you resend everything by mistake.
A bulk update needs no key: each item is idempotent for the same reasons the
single PUT is.
Limits
- 25 items per batch, 50 units per item.
- The bulk tier is 6 calls a minute, so a portfolio of 150 properties loads in one minute; see rate limits.
- Each created property sends its initial snapshot — around forty events — through your webhook, in sequence order. A batch of 25 queues roughly a thousand deliveries; expect them to drain over some minutes rather than instantly.
Errors
Request-level (nothing written):
Errors
| Status | Code | Meaning |
|---|---|---|
| 422 | VALIDATION_FAILED |
The request body failed validation. errors is Laravel's field bag: an object keyed by field path, each holding an array of messages. |
| 422 | IDEMPOTENCY_KEY_REQUIRED |
A create — POST /bookings, POST /properties or POST /properties/bulk — was sent without a usable Idempotency-Key header. |
| 422 | IDEMPOTENCY_KEY_REUSED |
The key has already been used, with a different request body. Doorloom fingerprints the body (SHA-256 over key-sorted JSON), so key order does not matter but any value change does. Keys are shared across every create endpoint, so a booking key resent on a property create is a reuse too. |
| 409 | IN_PROGRESS |
An identical request is still being processed. Two requests raced; the first one is still inside its transaction. |
| 429 | RATE_LIMITED |
You exceeded one of the rate limits. The standard Retry-After and X-RateLimit-* headers are set on the response. |
| 401 | UNAUTHENTICATED |
No API key, or a key that is not valid. Rotating your API key invalidates the previous one immediately. |
| 403 | NOT_INTEGRATION_TOKEN |
The bearer token is valid but is not an integration API key — it belongs to another Doorloom token surface. Integration keys are the only credential this API accepts. |
| 403 | INTEGRATION_DISABLED |
Doorloom staff disabled this integration. No events are recorded and no API call succeeds. This is distinct from paused: a paused integration keeps full API access and only stops receiving webhooks. |
Item-level (inside results[].error): PROPERTY_EXTERNAL_ID_EXISTS,
NOT_FOUND, PROPERTY_ARCHIVED and VALIDATION_FAILED for unit
rules — with the meanings on the single-endpoint pages.