Book a demo Start free trial
Developers

Conventions

The rules every endpoint follows, so you learn them once.

Versioning

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.

Shapes

  • Single resource: {"data": {…}}. Lists: {"data": […], "next_cursor": "…"|null, "has_more": bool, "limit": n}.
  • Field names are snake_case and stable. Every resource has id (UUID), object and created_at. A field that does not apply is present and null.
  • Timestamps are ISO-8601 in UTC: 2026-11-11T09:30:00Z. Dates are YYYY-MM-DD.
  • Money is an object: {"amount": "1250.00", "amount_minor": 125000, "currency": "GBP"}. Use amount_minor for arithmetic.

Pagination and updated_since

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

Errors: RFC 9457 problem+json

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…"
}
StatusWhen
400A POST without Idempotency-Key.
401No key, bad key, revoked, expired, wrong environment, outside the IP allow-list.
403The key lacks the scope the route needs (named in detail).
404No such resource in this agency. Another agency's id is a 404, never a 403: the API never confirms that a foreign record exists.
409A state rule refuses the change (for example a repair waiting for landlord approval).
412If-Match did not match the current ETag.
415A file type that is not allowed.
422Validation, with errors[]; or an Idempotency-Key reused with a different body.
429Rate limit. Honour Retry-After.
5xxOur fault. Retry with the same Idempotency-Key.

Idempotency

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.

ETags and If-Match

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.

Rate limits

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].

Files

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.

What we log

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>.