Conventions

Every rule on this page holds for every entity and every surface — theREST API, webhook payloads, and scheduled exports alike. Learn them once and you can write your deserialization layer, your types, and your error handling a single time instead of rediscovering them field by field. This is the canonical reference; the deep pages it links to cover the mechanics. (Webhooks are on theScale plan and above — see pricing.)

Identifiers

Every record carries two identifiers, and they are not interchangeable.

  • id — the PropSocket-minted canonical identifier for CDM records. It's a prefixed ULID: a per-entity prefix, then a 26-characterULID body (prp_ Property, unt_ Unit, res_ Resident,lse_ Lease, evt_ event, syj_ sync job,atm_ automation, aru_ automation run). This is the value you pass to/v1/{entity}/{id}. ULIDs are lexicographically sortable by creation time, so the prefix tells you the type at a glance and the body sorts in roughly chronological order.Note: organization_id and integration_id are plain UUIDs — they do not carry a prefix.
  • x_id — the native identifier from the source integration, carried through unmodified so you can cross-reference back to the system of record. It'sx_id, never source_id. It is only unique within an integration: the natural key for a record is the (x_id, integration_id) pair.
Identifiers on a Unit
{
  "id": "unt_01HX1H6U9P4LFXCZW3RNS5EDGB",
  "x_id": "entrata-unit-55812",
  "integration_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "property_id": "prp_01HX0G5T8N3KEWBYV2QMR4DCFA",
  "ps_synced_at": "2026-05-11T18:04:58Z"
}

PropSocket fields (ps_)

Fields prefixed ps_ are PropSocket operational metadata — values about our pipeline, never carried from a source. ps_ is reserved: no CDM-standard field and no source-native field will ever use it, so you can map ps_* fields once and trust they will never collide with source data. It is the mirror of x_, which marks raw values from the source. The first member is ps_synced_at — the UTC timestamp of the last successful sync that covered a record. Seeper-record freshness.

Money

Money is always an object: { "amount": 285000, "currency": "USD" }.amount is an integer in the currency's minor units285000 is $2,850.00 — and currency is an ISO 4217 code. We never use floats for money, because binary floats can't represent decimal cents exactly and the rounding drift compounds. Parse amount as an integer and divide by 100 only at display time.

Dates & times

Timestamps are UTC ISO 8601 with a trailing Z(2026-05-11T18:04:58Z) — never a local offset. Date-only fields, where there is no meaningful time component (a lease start_date, a move_in_date), areYYYY-MM-DD. Convert to the property's local timezone in your own layer if you need to; the wire format is always UTC.

Enums

Enum values are lowercase snake_case strings (active,month_to_month). Code defensively: we add new enum values additively as we map more of each source system, so treat any value you don't recognize as a valid-but-unknown member rather than an error. A switch with a sane default will not break when a new status appears.

Soft deletion

Records are never physically deleted. When a record disappears from the source, the next sync sets a non-null deleted_at timestamp and leaves the row in place. List endpoints exclude soft-deleted records by default; pass ?include_deleted=true to include them and check each record's deleted_at. See theCDM for the field-level contract and thesync engine for when the flag is set.

custom_data

Every entity has a custom_data object — a free-form JSON escape hatch for integration-specific fields the CDM doesn't standardize. It defaults to {}. Its contents vary by source and are not a stable contract: a field present for one integration may be absent for another, and we may begin standardizing a field into the CDM proper over time. Read it opportunistically; don't depend on a specific key being there.

The list envelope

Every list endpoint returns the same shape: a meta object describing the page and aresults array of records. Decide whether to fetch more frommeta.hasMore — don't infer "done" from a short page. Note the deliberate casing: it's hasMore, camelCase, even though record fields aresnake_casemeta describes the response, not the data. The default page limit is 25 and the max is 100 (asking for more returns a 400). Full mechanics and the stable-pagination pattern are inpagination.

meta + results
{
  "meta": {
    "limit": 25,
    "offset": 0,
    "hasMore": true
  },
  "results": [
    { "id": "unt_01HX1H6U9P4LFXCZW3RNS5EDGB" }
  ]
}

Filtering & sorting

Filters are query parameters. Different filters combine with AND; a single filter accepts a comma-separated list to match any of its values (OR within that field). There is no OR across different fields, no NOT, and no nesting. Numeric and date ranges use inclusive _gte / _lte pairs, and every entity supports time-window filters (updated_after, created_after, created_before). Sort with a single order-by=field:asc|desc — note the hyphen in the parameter name — and the default is created_at:desc. For stable, resumable paging, sort by created_at:asc. Naming a filter or sort field that doesn't exist returns a422 listing the valid options. The supported fields per entity are infiltering & sorting.

Errors

Errors are RFC 7807application/problem+json bodies: a type URL, a shorttitle, the HTTP status, a human-readable detail, and arequest_id you can quote to support. Validation errors (422) add anerrors array pinpointing each offending field. Theerrors reference lists every status code we return.

A 422 problem document
{
  "type": "https://docs.propsocket.io/errors/validation_error",
  "title": "Unprocessable Entity",
  "status": 422,
  "detail": "One or more query parameters failed validation.",
  "request_id": "req_01HX9P3K2N7QZRWY4B8MJ5VCDF",
  "errors": [
    { "pointer": "order-by", "detail": "Unknown field 'foo'. Valid: created_at, updated_at." }
  ]
}

Headers

Response headers are lowercase, hyphenated (kebab-case). Request headers you'll use most:

  • Idempotency-Keyrequired on all write requests(PATCH / POST to write endpoints). A client-generated string (UUID v4 recommended) that uniquely identifies this request's intent. Reusing the same key with an identical body returns the original operation with no duplicate work dispatched; reusing it with a different body returns 409 Conflict. Omitting the header on a write request returns 400 Bad Request. Seeasync write-back for the full contract.
  • x-request-id — present on all responses. A unique id for the request, echoing the request_id in any error body. Log it; quote it to support and we can find your exact request.

Rate-limit state rides on x-ratelimit-* headers, with a standardRetry-After on a 429 — see rate limits.

Backward compatibility

Changes within a version are additive: new fields, new enum values, and new endpoints can appear without notice, which is exactly why you code defensively against unknown enums and ignore unrecognized fields. Anything that could break an existing integration goes through the deprecation andbreaking-changes process first.

Next

See these conventions applied per entity in theCommon Data Model, how the cache stays current in thesync engine, or start reading in thequickstart.