Protocol

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: null are 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,
  • contactExternalIds on 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 address on an object clears the address link.
  • contactExternalIds is 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)