Skip to content

Launching soon: search & applications go live on September 30, 2026. Set up your profile now.

Sync listings from your CRM

Push and upsert listings into WOHNO from your own CRM/ERP using external_ref, idempotency keys and signed image uploads.

This guide shows how to keep WOHNO in sync with the listings you already manage in your own CRM or ERP — creating, updating and de-duplicating listings from a server, uploading images, and reacting to changes via webhooks.

The problem this solves: your source of truth is your CRM. You want every listing change there to flow into WOHNO automatically, idempotently, without creating duplicates on retries.

Generally available. The listings write surface (Plan 58) is live and documented in the public reference — no feature flag gates it. It is secret-key only; a publishable key is rejected with 403 INSUFFICIENT_SCOPE. The write operations are not part of the marketplace launch gate, so they are available before the public launch.

Prerequisites

  • A secret key (sk_live_…) — these endpoints are always sk-only. Publishable keys fail with 403 INSUFFICIENT_SCOPE.
  • Scope listings:write (which implies listings:read); listings:delete if you also remove listings.

Keep your secret key on the server. Never ship it in a browser bundle.

Step 1 — Upsert a listing keyed by your own ID

Send your CRM's stable identifier as external_ref. The first POST creates the listing; subsequent POSTs with the same external_ref update the existing one (org-scoped upsert), so you never create duplicates.

curl -X POST https://wohno.de/api/v1/listings \
  -H "X-API-Key: sk_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 7c9e6a3d-1f2b-4c8a-9d10-abc123def456" \
  -d '{
    "external_ref": "CRM-48217",
    "title": "Bright 2-room apartment",
    "street": "Beispielstraße",
    "house_number": "12",
    "zip_code": "10115",
    "city": "Berlin",
    "property_type": "apartment",
    "living_area": 58,
    "rooms": 2,
    "rent_cold": 980,
    "status": "draft"
  }'

Responses:

  • 201 Created with { "data": ListingPublicDto } on a fresh create.
  • 200 OK with { "data": ListingPublicDto } when the external_ref already exists (update/upsert).

status is draft (default) or active; only draft → active publishes the listing. The same moderation and geocoding pipeline as the dashboard wizard runs on every write.

Optional — assign the listing to a specific owner

By default a listing created via the API is owned by the user who created the API key. To assign it to a specific team member instead — for example the agent who manages it in your CRM — add their email as owner_email:

curl -X POST https://wohno.de/api/v1/listings \
  -H "X-API-Key: sk_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "external_ref": "CRM-48217",
    "title": "Bright 2-room apartment",
    "street": "Beispielstraße",
    "zip_code": "10115",
    "city": "Berlin",
    "property_type": "apartment",
    "living_area": 58,
    "rooms": 2,
    "rent_cold": 980,
    "owner_email": "[email protected]"
  }'
  • The email must belong to a member of the same organization (role owner, admin or member).
  • An unknown address or a non-member is rejected with 403 FORBIDDEN — deliberately with the same message, so the endpoint can't be used to probe which emails have a WOHNO account.
  • owner_email is honored only on create. On an external_ref upsert to an existing listing it is ignored — the owner is never re-homed.
  • Omit it to fall back to the API key's creator.

Tip: Point your secret key at a dedicated technical user. The listing owner is a real account; if a personal owner account is later removed, ownership does not transfer automatically. Setting owner_email per record keeps each listing attached to the right agent regardless of which key pushed it.

Step 2 — Make retries safe with Idempotency-Key

Pass a client-generated UUID v4 in the Idempotency-Key header (as above) and reuse it unchanged on network retries.

  • The key is valid for 24 hours. A retried request with the same key returns the same response without re-executing the action.
  • Sending the same key with a different body returns 409 IDEMPOTENCY_KEY_REUSED — one key is bound to exactly one request body.

Generate one key per logical CRM sync operation.

Step 3 — Update or transition the lifecycle

Use PATCH for partial updates. external_ref cannot be changed via PATCH. The lifecycle states rented and archived are reachable here:

curl -X PATCH https://wohno.de/api/v1/listings/9f1c2a3b-1111-2222-3333-444455556666 \
  -H "X-API-Key: sk_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "rent_cold": 950, "status": "active" }'

Re-geocoding only runs when you change the address. A draft → active transition emits listing.published.

Step 4 — Upload images via a signed URL

Image uploads use a two-step signed-URL flow — no large binary travels through the API. First request an upload URL:

curl -X POST https://wohno.de/api/v1/listings/9f1c2a3b-1111-2222-3333-444455556666/images \
  -H "X-API-Key: sk_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "filename": "living-room.jpg", "content_type": "image/jpeg", "size": 842133, "sort_order": 0 }'

Response (201):

{
  "data": {
    "image_id": "img_5f4e3d2c",
    "upload_url": "https://storage.wohno.de/...signed...",
    "upload_token": "...",
    "expires_at": "2026-06-04T12:10:00.000Z"
  }
}

Then PUT the binary directly to upload_url before expires_at. Allowed types are image/jpeg, image/png, image/webp; max size 5 MB. Remove an image with DELETE /api/v1/listings/{id}/images?image_id=img_5f4e3d2c.

Step 5 — Stay in sync via webhooks

Subscribe to listing.* events so your CRM learns about moderation outcomes and publish state changes pushed from WOHNO. See React to events with webhooks for the full setup; the relevant event types for this flow are listing.created, listing.published, listing.updated, listing.deleted, listing.moderated and listing.image.uploaded.

Error handling

CodeHTTPWhat happenedFix
INSUFFICIENT_SCOPE403Using a pk_ key or missing listings:writeUse a secret key with the write scope.
IDEMPOTENCY_KEY_REUSED409Same key, different bodyUse a fresh key per distinct payload.
VALIDATION_ERROR400Missing/invalid fields (see details.fields)Fix the body; the response lists the bad fields.
FORBIDDEN403Plan/quota limit (e.g. Free listing cap)Upgrade the plan or reduce volume.
FORBIDDEN403owner_email is not a member of your orgUse the email of an existing org member (owner/admin/member).
NOT_FOUND404Listing not found or belongs to another orgConfirm the listing ID and ownership.

See the conventions reference for idempotency and error details.

Best practices

  • One external_ref per CRM record. This is what makes the sync idempotent across full re-imports.
  • Batch carefully. The rate limit is 1000 requests/hour per key; respect Retry-After on 429.
  • Upload images after the listing exists, then flip status to active.

Next steps