Concepts

Upsert Pattern

One endpoint to create and update — no existence check needed

The API uses the upsert pattern: a single PUT request creates a new object or updates an existing one. You don't need to check if the object exists — the API handles it automatically.

Why This Matters

In a classic REST API, syncing data requires:

1. GET /listings/external/APT-001     # Check existence
2. If 404 → POST /listings            # Create
   If 200 → PUT /listings/42          # Update

With the upsert pattern, it's simpler:

1. PUT /listings { externalId: "APT-001", ... }
   # API creates or updates automatically

How It Works

SituationWhat Happens
Object not foundA new one is created
Object foundThe existing one is updated

Lookup happens by id or externalId — whichever you passed in the request.

Example: Creation

The first request with externalId: "APT-001" creates a listing:

curl -X PUT https://crm.rentix.md/api/v1/listings \
  -H "Authorization: ApiKey YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "externalId": "APT-001",
    "announcementType": "rent",
    "propertyType": "residential",
    "propertySecondaryType": "apartment",
    "announcementValue": 500,
    "announcementCurrency": "EUR"
  }'
Response
{
  "id": 42,
  "externalId": "APT-001",
  "status": "draft",
  "created": true,
  "updated": false
}

Note: created: true — the object was created.

Example: Update

A repeat request with the same externalId updates the listing:

curl -X PUT https://crm.rentix.md/api/v1/listings \
  -H "Authorization: ApiKey YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "externalId": "APT-001",
    "announcementValue": 600
  }'
Response
{
  "id": 42,
  "externalId": "APT-001",
  "status": "draft",
  "created": false,
  "updated": true
}

Now updated: true — the object was updated.

Partial Updates

When updating, pass only the changed fields. Others keep their values:

// First request — full creation
{
  "externalId": "APT-001",
  "announcementType": "rent",
  "announcementValue": 500,
  "propertyArea": 65,
  "propertyFloorNumber": 3
}

// Second request — change only the price
{
  "externalId": "APT-001",
  "announcementValue": 600
}
// propertyArea and propertyFloorNumber stay the same

Clearing Fields

To clear a field (remove its value), send null:

{
  "externalId": "APT-001",
  "propertyFloorNumber": null
}

After this request, propertyFloorNumber will be empty.

Benefits

Simple Integration

One endpoint for all cases. No need to write existence-check logic.

Idempotency

A repeat request with the same data won't create a duplicate — it just updates the existing object. This is important for network failures: you can safely retry requests.

Atomicity

Creation or update happens in a single transaction. There's no race condition like "checked — wasn't there, created — duplicate".

Bulk Upsert

The same principle works for bulk operations. Up to 100 objects per request. Each operation must include an op field:

curl -X POST https://crm.rentix.md/api/v1/listings/bulk \
  -H "Authorization: ApiKey YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "operations": [
      { "op": "upsert", "externalId": "APT-001", "announcementValue": 500 },
      { "op": "upsert", "externalId": "APT-002", "announcementValue": 700 },
      { "op": "upsert", "externalId": "APT-003", "announcementValue": 450 }
    ]
  }'
Response
{
  "results": [
    { "op": "upsert", "externalId": "APT-001", "id": 42, "success": true, "created": false, "updated": true },
    { "op": "upsert", "externalId": "APT-002", "id": 43, "success": true, "created": true, "updated": false },
    { "op": "upsert", "externalId": "APT-003", "id": 44, "success": true, "created": true, "updated": false }
  ],
  "summary": { "total": 3, "succeeded": 3, "failed": 0 }
}

Limitations

You cannot pass both id and externalId at the same time — this will cause an error. Choose one identification method.
ResourceEndpoint
ListingsPUT /listings
UsersPUT /users
Copyright © 2026