Troubleshooting

A symptom-first index of the errors engineers actually hit, with the exact response body togrep for. Every PropSocket response carries an x-request-id header, and error bodies repeat it as request_id. Capture that value — it's how support finds your exact request in seconds. The full status-code catalog lives inErrors; this page is the debugging companion.

401 Unauthorized

Symptom. Every request comes back 401, even ones you expect to work.

401 body
{
  "type": "https://docs.propsocket.io/errors/unauthorized",
  "title": "Unauthorized",
  "status": 401,
  "detail": "Authentication credentials were not provided.",
  "instance": "/v1/properties",
  "request_id": "req_01HX9P3K2N7QZRWY4B8MJ5VCDF"
}

Likely causes, most common first.

  • Missing the Bearer  prefix. The header must beAuthorization: Bearer ps_test_…, not the bare key.
  • The key was rotated or revoked in the dashboard and you're still using the old value.
  • A trailing newline or stray quote got into the env var when you exported it.

Fix & confirm. Re-export the key from a fresh dashboard copy, then re-run the quickstart's first call. Both ps_test_ and ps_live_ keys hit the same host, api.propsocket.io — a clean GET /v1/properties returning 200 confirms the key is valid. The auth model is in Authentication.

403 Forbidden

Symptom. A valid key authenticates, but a specific resource is refused.

403 body
{
  "type": "https://docs.propsocket.io/errors/forbidden",
  "title": "Forbidden",
  "status": 403,
  "detail": "You do not have permission to access this resource.",
  "instance": "/v1/properties/prp_01HX0G5T8N3KEWBYV2QMR4DCFA",
  "request_id": "req_01HX9P3K2N7QZRWY4B8MJ5VCDF"
}

Likely cause. 403 is reserved for explicit permission denials. In practice you are far more likely to see 404 than 403 when you reach for a resource your Organization doesn't own: PropSocket scopes every query to your Organization and filters the record out before lookup, so cross-org access reads as "not found," not "forbidden." This is a deliberate information-leak defense — we don't confirm that another Organization's record exists.

Fix & confirm. If you got a 403, check what the endpoint actually requires (some administrative surfaces are tighter than the read API). If you expected the record and got a 404 instead, jump to the 404 section below — that's the cross-org case.

404 Not Found

Symptom. A record you're sure exists comes back 404.

404 body
{
  "type": "https://docs.propsocket.io/errors/not_found",
  "title": "Not Found",
  "status": 404,
  "detail": "Property with id 'prp_01HX0G5T8N3KEWBYV2QMR4DCFA' was not found.",
  "instance": "/v1/properties/prp_01HX0G5T8N3KEWBYV2QMR4DCFA",
  "request_id": "req_01HX9P3K2N7QZRWY4B8MJ5VCDF"
}

Likely causes.

  • The record was soft-deleted. List and retrieve endpoints exclude soft-deleted records by default. The record is still there with deleted_at populated — you just can't see it without asking.
  • The record belongs to another Organization. Your key only sees your Organization's data; everything else reads as 404 (see 403 above).
  • The id is wrong — a typo, or a stale id from a different Organization.

Fix & confirm. Add ?include_deleted=true to the list call. If the record reappears with a non-null deleted_at, it was soft-deleted — a removal in the source system, not a bug. If it still doesn't appear, the id belongs to another Organization or another environment.

422 Unprocessable Entity

Symptom. A well-formed request is rejected for a semantic reason — a bad sort field, an out-of-range limit, a malformed date.

422 body
{
  "type": "https://docs.propsocket.io/errors/validation_error",
  "title": "Unprocessable Entity",
  "status": 422,
  "detail": "One or more query parameters failed validation.",
  "instance": "/v1/leases",
  "request_id": "req_01HX9P3K2N7QZRWY4B8MJ5VCDF",
  "errors": [
    { "pointer": "order-by", "detail": "Unknown field 'foo'. Valid: created_at, updated_at, start_date." },
    { "pointer": "limit", "detail": "Ensure this value is less than or equal to 100." }
  ]
}

Likely cause & fix. Read the errors[] array — each entry names the offending parameter in pointer and explains it in detail. Common cases:

  • limit above 100 — the max page size is 100.
  • order-by referencing a field that entity doesn't sort on — the message lists the valid fields. See Filtering & sorting.
  • A date filter not in YYYY-MM-DD form.

Confirm. Fix the named parameter and re-run; a 200 confirms it. Iferrors[] points at a parameter you didn't send, check for a client library silently appending defaults.

429 Too Many Requests

Symptom. Requests start failing with 429 under load — typically during a backfill or a burst of parallel calls.

429 response (headers + body)
HTTP/1.1 429 Too Many Requests
x-ratelimit-limit: 150
x-ratelimit-remaining: 0
Retry-After: 23
Content-Type: application/problem+json

{
  "type": "https://docs.propsocket.io/errors/rate_limit_exceeded",
  "title": "Too Many Requests",
  "status": 429,
  "detail": "Rate limit exceeded. Retry after 23 seconds.",
  "instance": "/v1/units",
  "request_id": "req_01HX9P3K2N7QZRWY4B8MJ5VCDF"
}

Likely cause. The limit is 150 requests per minute per Organization— every API key in your Organization shares one pool. Two services hammering the API on the same Organization compete for the same 150. PropSocket adds a 2-second delay at 80% utilization before it ever returns a hard 429 at 100%.

Fix & confirm. Read Retry-After on the 429 and sleep for exactly that many seconds — don't guess a backoff. Watch x-ratelimit-remaining on every response to throttle proactively. A backfill that honors Retry-After runs to completion without manual tuning — see Rate limits and thebackfill recipe.

Webhook signature mismatch

Webhooks are on the Scale plan and above (Scale and Enterprise) — seepricing.

Symptom. Your receiver computes a digest that never equalsX-PropSocket-Signature, so every event is rejected — even though the secret looks right.

Likely cause, by far the most common. You're hashing a re-serialized body. If your framework parsed the JSON into an object and you re-stringified it before computing the HMAC, the bytes changed — key order and whitespace differ — and the digest won't match. The signature is over the raw, unmodified request body.

Checklist.

  • Capture the raw body before any JSON parsing. In Express: express.raw({ type: "application/json" }). In Flask: request.get_data(). Don't read a parsed/re-encoded body.
  • Confirm the secret is the exact value from the subscription (whsec_…), not your API key. They are different secrets.
  • Compare hex strings, and use a constant-time compare. A length mismatch alone fails the compare.
  • If you recently rotated the secret, you're in the grace window: events are signed with both secrets and you'll also receive X-PropSocket-Signature-Previous. Verify against either.

Confirm. Recompute the digest by hand from the raw bytes and diff it against the header:

Recompute the signature by hand
# The signature is HMAC-SHA256 over the RAW request body. Re-print exactly
# what your framework gave you BEFORE any parsing, and recompute by hand:
printf '%s' "$RAW_BODY" | openssl dgst -sha256 -hmac "$PROPSOCKET_SIGNING_SECRET"
# Compare the hex digest to the X-PropSocket-Signature header value.

If the hand-computed digest matches the header but your code's doesn't, your code is mutating the body before hashing. The verified implementations in seven languages are inWebhooks → Verify the signature.

Sync stuck or lagging

Symptom. New data in the PMS isn't showing up over the API, orSYNC_COMPLETE events stopped arriving.

Likely cause. You're reading PropSocket's normalized cache, which refreshes on atier-controlled cadence — every 4 hours on the entry tier, down to 5 minutes at the top tier. Data that changed in the PMS between syncs simply isn't visible yet. That's expected behavior, not a stuck sync. A genuinely stuck sync is rarer: a failed run, or a connector hitting the source PMS's own rate limits.

Fix & confirm.

  • Check Dashboard → Integrations → Sync history for the last successful run and its timestamp. If the last run is within your cadence window, the data simply isn't due yet.
  • Subscribe to SYNC_COMPLETE and use it as a checkpoint — it fires once per run after all data-change events, summarizing records new/updated/soft-deleted.
  • If a run is marked failed, the dashboard shows the error. Connector-side rate limiting against the source PMS resolves on the next scheduled run.

Initial syncs are silent by design — the bulk load fires only FIRST_SYNC_COMPLETE, no per-record events. If you connected a source and saw no entity events, that's why.

Still stuck?

Open a support ticket and include the x-request-id (or therequest_id from the error body) for the failing call. For webhook problems, include the event id from Dashboard → Webhooks → Recent deliveries. Those two values cut debugging from minutes to seconds.