deep.rent Object API
Public reference for pushing contacts, objects, and addresses from your system into deep.rent via API keys.
Base URL: https://api.deep.rent (replace with your environment, e.g. https://api-dev.deep.rent).
Overview
Use the Object API when your system (CRM, ERP, property management software) is the source of truth and wants to sync its portfolio into deep.rent. You push entities under your own ids (externalId); deep.rent derives stable internal ids from them, so every push is idempotent — repeating a request updates instead of duplicating.
The core idea: one PUT per entity with the complete representation of that entity. No mapping layer, no import wizard, no file uploads — one request, one definitive result.
What you can push
| Use case | How it works |
|---|---|
| Sync a contact (tenant, owner, partner) | PUT /external/v1/contacts/:externalId with name/email/phone. |
| Sync a property or unit | PUT /external/v1/objects/:externalId with name, type, and address. |
| Attach an address to an object | Nest address inside the object body. It becomes a real address record. |
| Link contacts to an object | Send contactExternalIds on the object (push the contacts first). |
| Sync many entities at once | POST .../batch with up to 100 items and per-item results. |
| Remove an entity | DELETE /external/v1/{contacts|objects}/:externalId — safe archive (soft delete). |
| Verify what deep.rent stored | GET /external/v1/{contacts|objects}/:externalId. |
How pushed data appears in the Protocol App
| Entity | Visibility |
|---|---|
| Contacts | Appear automatically in the Contacts workspace after the next app sync. |
| Addresses | Appear automatically together with their object. |
| Objects | Appear as importable rows in the objects list. A user picks an object to start working with it — same flow as CRM integrations like Propstack. Later pushes update the imported object. |
Typical sync flow
1. Push the contacts you want to link
PUT /external/v1/contacts/:externalId (or POST /external/v1/contacts/batch)
2. Push the objects, referencing those contacts
PUT /external/v1/objects/:externalId (or POST /external/v1/objects/batch)
3. Optional: verify a single entity
GET /external/v1/objects/:externalId
4. On deletion in your system: archive in deep.rent
DELETE /external/v1/objects/:externalId
Minimal example
PUT /external/v1/objects/building-42
Content-Type: application/json
x-api-key: YOUR_API_KEY
{
"name": "Easy Living 12",
"objectCategory": "APARTMENT",
"customerObjectId": "EL-12",
"address": {
"addressLine1": "Main Street 14",
"postalCode": "80331",
"city": "Munich",
"country": "Germany",
"countryCode": "DE"
},
"contactExternalIds": ["crm-contact-1"]
}
Response 201:
{
"externalId": "building-42",
"localId": "ORG_ID:external:object:building-42",
"status": "created",
"entity": { "...": "the stored object" }
}
Main endpoint groups
| Area | Endpoint |
|---|---|
| Contacts | /external/v1/contacts/:externalId, /external/v1/contacts/batch |
| Objects | /external/v1/objects/:externalId, /external/v1/objects/batch |
Authentication
API key (/external/*)
Send your API key on every request:
x-api-key: YOUR_API_KEY
Or:
Authorization: Bearer YOUR_API_KEY
- Each key is bound to one organization. You cannot override the organization with
x-org-id. - The key owner must be a member of that organization.
- Keys with
permissions: nullare legacy unrestricted keys.
Permissions
| Permission | Allows |
|---|---|
integration:read |
Verification reads (GET) |
integration:write |
Upsert, batch upsert, and archive |
Missing permission returns:
{
"status": 403,
"message": "Insufficient API key permissions",
"required": "integration:write"
}
Core concepts
External ids
Every entity is addressed by the id from your system. Allowed characters: letters, digits, ., _, @, - (1–200 characters). deep.rent derives a deterministic internal id from your externalId, so:
- repeating a push converges on the same record (idempotent upsert),
- two different organizations can use the same external ids without collisions,
contactExternalIdson objects resolve without any lookup round-trip.
Declarative upserts
Every PUT body is the complete representation of the entity:
- Omitting an optional field clears it (sets it to empty).
- Omitting
addresson an object clears the address link. contactExternalIdsis tri-state: omitted = leave contact links unchanged,[]= remove all links,[ids]= replace links.
Idempotency and unchanged
deep.rent hashes each push payload. Re-sending an identical body skips all writes and responds with status: "unchanged" — safe and cheap for full-portfolio re-syncs.
status |
Meaning |
|---|---|
created |
Entity did not exist and was created (HTTP 201) |
updated |
Entity existed and was updated (HTTP 200) |
unchanged |
Identical payload already stored; nothing written (HTTP 200) |
Strict validation
Request bodies are validated strictly: unknown fields are rejected with 400 validation_error. This protects you from silent typos (fistName fails instead of being ignored).
Contacts
Upsert contact
PUT /external/v1/contacts/:externalId
Content-Type: application/json
x-api-key: YOUR_API_KEY
{
"contact": {
"salutation": "MR",
"personTitle": null,
"firstName": "Max",
"lastName": "Mustermann",
"email": "[email protected]",
"phoneNumber": "+49 30 1234567",
"companyName": null
},
"details": {
"language": "de",
"role": "RENTER"
},
"address": {
"addressLine1": "Main Street 14",
"postalCode": "80331",
"city": "Munich",
"country": "Germany",
"countryCode": "DE"
}
}
Field rules:
| Field | Required | Notes |
|---|---|---|
contact |
yes | At least one of firstName, lastName, companyName, email must be set. |
details |
no | language, role. |
address |
no | Free-form contact address; all fields optional. |
Response 201 (created) / 200 (updated or unchanged):
{
"externalId": "crm-contact-1",
"localId": "ORG_ID:external:contact:crm-contact-1",
"status": "created",
"entity": { "...": "the stored contact" }
}
Batch upsert contacts
POST /external/v1/contacts/batch
Content-Type: application/json
x-api-key: YOUR_API_KEY
{
"items": [
{ "externalId": "crm-contact-1", "data": { "contact": { "firstName": "Max", "lastName": "Mustermann" } } },
{ "externalId": "crm-contact-2", "data": { "contact": { "companyName": "Hausverwaltung Nord GmbH" } } }
]
}
Maximum 100 items per request. The response always returns 200 with one result per item; failed items do not abort the batch:
{
"items": [
{ "externalId": "crm-contact-1", "status": "created", "localId": "ORG_ID:external:contact:crm-contact-1" },
{ "externalId": "crm-contact-2", "status": "error", "error": "validation_error", "message": "..." }
]
}
Read contact
GET /external/v1/contacts/:externalId
x-api-key: YOUR_API_KEY
Response 200: { "entity": { ... }, "link": { ... } } where link contains sync metadata (lastPushedAt, payloadHash, archivedAt). Returns 404 when the id was never pushed.
Archive contact
DELETE /external/v1/contacts/:externalId
x-api-key: YOUR_API_KEY
Soft delete: the contact is archived, not destroyed. Returns 204. Re-pushing the same externalId later restores it.
Objects
Upsert object
PUT /external/v1/objects/:externalId
Content-Type: application/json
x-api-key: YOUR_API_KEY
{
"name": "Easy Living 12",
"objectCategory": "APARTMENT",
"objectType": "FLAT",
"customerObjectId": "EL-12",
"address": {
"addressLine1": "Main Street 14",
"addressLine2": null,
"postalCode": "80331",
"city": "Munich",
"country": "Germany",
"countryCode": "DE",
"administrativeArea": null
},
"contactExternalIds": ["crm-contact-1", "crm-contact-2"]
}
Field rules:
| Field | Required | Notes |
|---|---|---|
name |
yes | Display name of the object. |
objectCategory, objectType, customerObjectId |
no | Free classification strings. |
address |
no | Becomes a real address record. When present, addressLine1, country, countryCode are required. Omitting address clears the object's address. |
contactExternalIds |
no | Tri-state (see Core concepts). Referenced contacts must be pushed first, otherwise 400 invalid_contact_external_ids. |
Response 201 (created) / 200 (updated or unchanged) — same envelope as contacts.
Batch upsert objects
POST /external/v1/objects/batch
Content-Type: application/json
x-api-key: YOUR_API_KEY
Same shape and rules as the contacts batch: { "items": [{ "externalId", "data" }] }, max 100 items, per-item results.
Read object
GET /external/v1/objects/:externalId
x-api-key: YOUR_API_KEY
Archive object
DELETE /external/v1/objects/:externalId
x-api-key: YOUR_API_KEY
Soft delete, returns 204. Archiving an object never deletes its linked contacts or address.
Limits and fair use
| Limit | Default | On violation |
|---|---|---|
| Writes per API key | 120 per minute | 429 rate_limited with Retry-After |
| Writes per organization | 300 per minute | 429 rate_limited |
| Batch size | 100 items | 400 validation_error |
| Request body size | 1 MB | 413 payload_too_large |
A batch request counts as its item count against the rate limits (a 100-item batch consumes 100 writes). Unchanged re-pushes still count as requests — but they are cheap, so full re-syncs are fine when paced within the limits.
Error handling
Errors are returned as JSON. Common responses:
| Status | Error | Meaning |
|---|---|---|
400 |
validation_error |
Body failed strict validation (details included) |
400 |
invalid_external_id |
externalId empty, too long, or contains illegal characters |
400 |
invalid_contact_external_ids |
Object references contacts that were not pushed (or are archived) |
401 |
— | API key missing or invalid |
403 |
— | Key lacks the required integration permission |
404 |
not_found |
Entity was never pushed under this externalId |
409 |
conflict |
Conflicting unique value, e.g. duplicate customerObjectId |
413 |
payload_too_large |
Request body exceeds the size limit |
429 |
rate_limited |
Rate limit exceeded; retry after the Retry-After interval |
500 |
internal_error |
Internal server error |
Validation example:
{
"error": "validation_error",
"details": [
{ "path": "contact.email", "message": "Too big: expected string to have <=320 characters" }
]
}
Relationship to the Contacts API
The existing /external/contacts endpoints remain available and are ideal when deep.rent generates the ids or when you manage contacts for portal delivery. The Object API is the better fit when your system owns the ids and you sync objects too:
/external/contacts |
/external/v1 Object API |
|
|---|---|---|
| Id ownership | Yours or server-generated | Always yours (externalId) |
| Objects & addresses | No | Yes |
| Idempotent re-sync | By your id | By your id + payload hash (unchanged) |
| Strict validation | Lenient | Strict (unknown fields rejected) |
| Batch | No | Yes (100 items) |