Skip to main content
Seeki.eu

Agency Import API

Push listings into Seeki via API.

Bulk-import real-estate listings from your CRM or property management system. One endpoint, per-item status, automatic TTL.

Get an API key

Access to the Agency Import API requires an active Seeki agency subscription. Each subscription comes with one API key scoped to your agency account.

Subscribe on the For Agents page to get started.

Key management UI (view, rotate, revoke keys) is coming in a future dashboard update. For now, your API key is emailed when your subscription is activated. Contact info@seeki.eu if you need a new key.

Authentication

Every request must include your API key as a Bearer token in the Authorization header.

HTTP header
Authorization: Bearer seeki_live_your_key_here

Keys starting with seeki_live_ are production keys. Staging keys start with seeki_test_. They are not interchangeable across environments.

Your first import

Send a POST request to /v1/listings with a JSON body containing a listings array. Each element represents one property.

curl
curl -X POST https://ingest.seeki.eu/v1/listings \
  -H "Authorization: Bearer seeki_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "listings": [
      {
        "external_id": "crm-listing-00123",
        "listing_type": "SELL",
        "real_estate_type": "APARTMENT",
        "price": 320000,
        "currency": "EUR",
        "address": {
          "street": "Wenceslas Square 1",
          "city": "Prague",
          "country_code": "CZ",
          "postal_code": "11000"
        },
        "name": "Sunny 2-bedroom apartment, Wenceslas Square",
        "floorage": 65
      }
    ]
  }'

A successful batch returns HTTP 200 with accepted and rejected arrays:

200 OK
{
  "accepted": [
    { "external_id": "crm-listing-00123", "import_item_id": "uuid-…" }
  ],
  "rejected": []
}

Request shape

The top-level body is { listings: [...] }. Required fields per listing:

FieldRequired?Notes
external_idRequiredYour CRM's unique ID. Used for idempotent re-imports and deduplication.
listing_typeRequiredSELL or RENT.
real_estate_typeRequiredAPARTMENT, HOUSE, LAND, COMMERCIAL, or OTHER.
priceRequiredNumeric. Use currency field for the ISO 4217 code (defaults to EUR).
address OR coordinatesRequiredProvide either an address object (street, city, country_code) or coordinates ({ lat, lng }). At least one must be present.
currencyOptionalISO 4217 code, e.g. EUR, CZK, PLN. Defaults to EUR.
name, descriptionOptionalListing title and free-text description.
floorage, floor, roomsOptionalProperty size in m², floor number, and room count.
imagesOptionalArray of public image URLs. The import worker fetches and caches them.
contactOptionalAgent name, email, and phone for inquiry routing.

See the full schema with all optional fields in the interactive API reference.

Response shape & errors

The /v1/listings endpoint always returns HTTP 200 OK as long as the request itself is well-formed (valid JSON, correct auth header, within size limits). Individual listing failures are reported inside the rejected array — not as HTTP error codes.

Each rejected item includes external_id, error_code, and a human-readable message. Batch-level errors (bad auth, oversized payload) return a non-200 HTTP status with a top-level error_code.

Error codes

Batch-level codes return HTTP errors. Per-item codes appear in the rejected array of a 200 response.

error_codeHTTP / scopeMeaning
MISSING_AUTHORIZATION401No Authorization header was sent.
INVALID_KEY401The key was not found, has been revoked, or has expired.
SUBSCRIPTION_INACTIVE403The agency's subscription is not active or past_due.
PAYLOAD_TOO_LARGE413Request body exceeds the 25 MB limit.
TOO_MANY_ITEMS413The listings array contains more than 5,000 items.
VALIDATION_FAILEDper-itemOne or more required fields are missing or have an invalid value. Check the message field for details.
GEOCODE_ADDRESS_NOT_FOUNDper-itemThe provided address could not be resolved to coordinates. Verify street, city, and country_code.
GEOCODE_COORDINATES_INVALIDper-itemThe provided lat/lng are outside valid ranges (lat ±90, lng ±180).
INFRA_QUEUE_REJECTEDper-itemThe workflow queue rejected the item, usually due to a transient overload. Retry after a short delay.
INFRA_LOSTper-itemThe workflow was accepted but its result was never recorded. Use the retry endpoint.
INTERNAL_ERRORper-itemAn unexpected server error occurred. Retrying may succeed; contact support if it persists.

Idempotency / re-imports

Re-posting a listing with the same external_id updates the existing record — it does not create a duplicate. The listing's TTL is refreshed on every successful import.

Internally, each import item maps to a Cloudflare Workflow with the ID:

Workflow ID pattern
agency-{agency_id}-{external_id}

Listing expiration

Every imported listing has a TTL of 60 days from the most recent successful import. Once the TTL expires, the listing is automatically unpublished by a daily cron job.

To keep a listing live indefinitely, re-import it (even with identical data) before the 60-day window closes. Implementing a nightly re-sync of your active listings is the recommended pattern.

Status + retry

Use GET /v1/import-items/{import_item_id}/status to poll the processing state of a single item. The import_item_id is returned in the accepted array of the original POST response.

GET /import-items/{id}/status
curl https://ingest.seeki.eu/v1/import-items/{import_item_id}/status \
  -H "Authorization: Bearer seeki_live_your_key_here"
Response
{
  "import_item_id": "uuid-…",
  "external_id": "crm-listing-00123",
  "status": "completed",
  "listing_id": "uuid-listing"
}

If an item ends up in an INFRA_LOST or INFRA_QUEUE_REJECTED state, re-queue it without resubmitting the full batch:

POST /import-items/{id}/retry
curl -X POST https://ingest.seeki.eu/v1/import-items/{import_item_id}/retry \
  -H "Authorization: Bearer seeki_live_your_key_here"

Limits

  • Body size: 25 MB maximum per request.
  • Items per request: 5,000 maximum. Split larger batches.
  • No QPS limit is enforced today. Very large bursts may be rate-limited automatically; spread high-volume syncs over multiple requests.

OpenImmo XML / zip intake

OpenImmo is the de-facto real-estate exchange format for the DACH market (Germany, Austria, Switzerland) and is produced by every major CRM in those countries — FlowFact, onOffice, ImmoMaster, edomi, and others. If your CRM already exports OpenImmo, you can POST it directly without remapping fields into Seeki-JSON.

Two delivery flavours are accepted on the same endpoint, distinguished byContent-Type:

  • application/xml — raw OpenImmo XML, images referenced by absolute URL.
  • application/zip — bundle containing index.xml at the root plus the JPGs referenced from <anhang gruppe="INTERN"> nodes. We extract every referenced image, host it on Supabase Storage, and rewrite the listing’s media.images array before publication. Hosted images are removed when the listing expires (60-day default TTL); re-imports refresh them.
curl — XML
curl -X POST https://ingest.seeki.eu/v1/openimmo \
  -H "Authorization: Bearer seeki_live_your_key_here" \
  -H "Content-Type: application/xml" \
  --data-binary @export.xml
curl — zip
curl -X POST https://ingest.seeki.eu/v1/openimmo \
  -H "Authorization: Bearer seeki_live_your_key_here" \
  -H "Content-Type: application/zip" \
  --data-binary @export.zip

Target spec version: OpenImmo 1.2.x. Body cap: 200 MB. Bigger feeds get a 413 with a Suggested-Chunk-Size: 500 header — split index.xml into multiple POSTs of ≤500 listings each. Spec reference: openimmo.de.

The same per-item error codes from the JSON intake apply — only the request body format differs. Each <immobilie> that fails Zod validation appears in the response’s rejected array with a VALIDATION_FAILED code and the offending field path.

Idealista XML-ML intake

Idealista’s XML-ML (xmlGenerator) is the standard feed format on the Iberian peninsula — used by Idealista itself and the CRMs that export to it for the Spanish (ES) and Portuguese (PT) markets. Country is inferred per record from the <provincia> field; locale follows country.

Images are referenced by URL (<imagenes><imagen url="…"/>), so there is no zip flavour for this format — application/xml only. Listings without an accessible image URL are still accepted; only fields that fail Zod validation are rejected.

curl
curl -X POST https://ingest.seeki.eu/v1/idealista \
  -H "Authorization: Bearer seeki_live_your_key_here" \
  -H "Content-Type: application/xml" \
  --data-binary @feed.xml

Target spec: Idealista xmlGenerator-1.X. The XML prolog encoding declaration is honoured — feeds shipped as windows-1252 are decoded correctly without manual conversion. Body cap: 200 MB. Spec reference: search Idealista’s help centre for “XML feed schema”.

Same per-item error codes as the JSON intake — see the table above.

Going live

There are two separate environments. API keys are environment-scoped — a production key will not work on staging, and vice versa.

EnvironmentBase URL
Staginghttps://ingest.seeki.store/v1
Productionhttps://ingest.seeki.eu/v1

All endpoints (/listings, /import-items/{id}/status, /import-items/{id}/retry) are identical across both environments.

Ready to integrate?

Open the interactive reference to explore all fields, try requests in the browser, and download the JSON Schema for your validator.