The rules every endpoint follows, so you learn them once.
The version is in the path: https://app.letfold.com/api/v1. Within v1 we only add: new fields, new endpoints, new event types, new enum values. Removing or renaming anything, or changing a type, means v2. Deprecations are announced on the changelog and emailed to each key's developer address at least 90 days ahead.
{"data": {…}}. Lists: {"data": […], "next_cursor": "…"|null, "has_more": bool, "limit": n}.snake_case and stable. Every resource has id (UUID), object and created_at. A field that does not apply is present and null.2026-11-11T09:30:00Z. Dates are YYYY-MM-DD.{"amount": "1250.00", "amount_minor": 125000, "currency": "GBP"}. Use amount_minor for arithmetic.Every list takes ?limit= (1 to 100, default 25) and returns next_cursor while has_more is true. Pass it back as ?cursor=. Cursors are opaque; order is creation order, so a page never shows a row twice and never skips one. Every list also takes ?updated_since=2026-11-10T00:00:00Z for incremental sync.
GET /properties?limit=50
GET /properties?limit=50&cursor=eyJjIjoiMjAyNi0xMS0xMSAwOTozMDowMCIsImlkIjoi…
GET /leads?updated_since=2026-11-10T00:00:00Z
Every error has Content-Type: application/problem+json and this body. Quote request_id when you write to us; it is also in the X-Request-Id header of every response, success or not.
{
"type": "https://letfold.com/developers/conventions#errors/validation",
"title": "Validation failed",
"status": 422,
"detail": "One or more fields are invalid.",
"instance": "/api/v1/leads",
"errors": [{ "field": "email", "message": "Give an email address or a phone number." }],
"request_id": "a2c4…"
}
| Status | When |
|---|---|
| 400 | A POST without Idempotency-Key. |
| 401 | No key, bad key, revoked, expired, wrong environment, outside the IP allow-list. |
| 403 | The key lacks the scope the route needs (named in detail). |
| 404 | No such resource in this agency. Another agency's id is a 404, never a 403: the API never confirms that a foreign record exists. |
| 409 | A state rule refuses the change (for example a repair waiting for landlord approval). |
| 412 | If-Match did not match the current ETag. |
| 415 | A file type that is not allowed. |
| 422 | Validation, with errors[]; or an Idempotency-Key reused with a different body. |
| 429 | Rate limit. Honour Retry-After. |
| 5xx | Our fault. Retry with the same Idempotency-Key. |
Every POST needs an Idempotency-Key header (any unique string up to 255 characters; a UUID is ideal). For 24 hours the same key with the same body returns the stored response with Idempotent-Replayed: true, and the same key with a different body is a 422. Keys are scoped to your API key. A 5xx answer is not stored, so retrying with the same key is safe.
Every single-resource GET carries an ETag. Send it back as If-None-Match to get a 304 when nothing changed, or as If-Match on a PATCH to be refused with 412 if someone else changed the record first.
600 requests a minute per key, and separately 100 writes (POST, PATCH, DELETE) a minute per key. Every response carries X-RateLimit-Limit and X-RateLimit-Remaining; a 429 adds Retry-After and X-RateLimit-Reset. Need more? Write to [email protected].
Photos and documents are served through short-lived signed URLs (60 and 15 minutes). The URL is the credential: fetch it with no header. Ask for the resource again when it expires.
Method, path, status, duration, IP and request id of every call, for 90 days. Never a request or response body. Every write is stamped in the agency's audit trail as api:<key name>.