Skip to content
Letted DocsGo to Letted

Connection reference

The details to give your software provider or the person building your connection.

You can copy this page into your implementation brief. For plain-English instructions, start with our getting started guide.

Address and authentication

The API base address is https://app.letted.com/api/v1. Requests use a private API key in the Authorization header. Never put the key in the URL or in public browser code.

curl 'https://app.letted.com/api/v1/properties' \
  --header 'Authorization: Bearer YOUR_API_KEY'

For POST and PATCH requests, also send Content-Type: application/json, a JSON body and Idempotency-Key. Examples with uppercase placeholders are templates; substitute real IDs obtained by lookup. JSON examples containing repeated-digit UUIDs are fictional.

Routes and permissions

Read information

Template lists support only limit and after. Agreement lists also accept tenancyId, and support externalId for agreement references. Other lists support limit (1–100, default 50), after, propertyId and externalId. Use externalId for references supplied when creating tenancies, tenants or tasks. Lists return data and nextCursor; individual lookups return data. All responses also include requestId.

Create and change information

Write permissions do not grant read permissions. Every route enforces organisation and property access. General tenancy editing, deletion and task-update routes are not available.

Avoid duplicates when retrying

Every POST and PATCH request must include an Idempotency-Key: a unique label for one intended action, containing 1–200 printable non-space ASCII characters.

Idempotency-Key: crm-tenancy-1042-create-v1

If the connection times out, retry with the same key, method, path and unchanged body. Keep the same JSON serialisation, including field order. Letted returns the saved result rather than repeating the saved change.

Use a new key for a genuinely new action. Reusing a key for a different request returns HTTP 409. Idempotency is scoped to the API key, so switching API keys does not preserve retry protection.

An externalId serves a different purpose: it links a record to a reference in your other system. It is unique per organisation and resource type, is at most 200 characters, and cannot be used to silently overwrite a record.

Tenancy fields

Required: propertyId, startDate, rentAmount and rentDueDate.

Optional fields:

Dates use YYYY-MM-DD. Monetary input values are numbers in pounds with no more than two decimal places. Rent and deposit amounts cannot be negative. Monetary values read back from the API may be decimal strings; do not assume every returned amount is a JSON number.

An optional monthly-charge example:

{
  "monthlyCharges": [{ "label": "Parking", "amount": 40 }]
}

This is a field fragment to include in a complete tenancy-creation body, not a standalone request.

Tenant fields

A new tenant needs name (1–200 characters). Optional fields are email, phone, portalAccessEnabled, dateOfBirth, emergencyContact, employmentInfo and externalId. New-tenant portal access defaults to false.

Inside a tenancy-group request, use isLeadTenant: true for at most one person. To link an existing tenant, supply tenantId and optionally isLeadTenant instead of new-person fields. Linking does not reset their access.

Tenant PATCH accepts any subset of the contact/access fields, but not externalId, tenantId or isLeadTenant. Email, phone, date of birth, emergency contact and employment information accept null to clear them. Enabling access for an unlinked account requires a valid email.

Agreements, forms and custom fields

For POST /tenancies/{id}/agreements:

| Property | Required | Meaning | | ------------------- | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | templateId | One source required | UUID of a saved template owned by the API key creator. Cannot be combined with document. | | document | One source required | Inline document with name, markdown and optional fields. Creates a one-off document, not a saved template. | | signatureRequired | No | Boolean override; omitted inherits the saved template setting, or defaults to true for an inline document. Set false for a form completed without signatures. | | externalId | No | Your agreement lookup reference, up to 200 characters. Does not insert text into the contract. | | fieldValues | When required by the fields | Object mapping declared custom field keys to single-line plain-text strings. |

When using a saved template, read GET /agreement-templates/{id} first. Its data.fields entries contain key, label, filledBy (landlord or tenant) and required. Use the key without placeholder braces in fieldValues; keys are case-sensitive. For a template declaring tenancyReference and occupantId:

{
  "templateId": "11111111-1111-4111-8111-111111111111",
  "externalId": "crm-agreement-1042",
  "fieldValues": {
    "tenancyReference": "CRM-TENANCY-1042",
    "occupantId": "OCC-27"
  }
}

For an inline document, document.name is a non-blank string up to 120 characters, document.markdown is non-blank Markdown up to 500,000 characters, and document.fields defaults to []. The total HTTP request must still fit the 128,000-byte body limit. signatureRequired belongs at the top level, not inside document.

Each field definition needs a unique key, a non-blank label up to 120 characters, and filledBy: "landlord" or "tenant". required is a boolean defaulting to true. Every declared key must appear as {{key}} in the Markdown. Unknown placeholders, unused definitions and duplicate or reserved keys are rejected. Saved-template requests use the template's definitions; do not send replacement fields alongside templateId.

{
  "document": {
    "name": "Occupant information",
    "markdown": "# Occupant information\nProperty: {{propertyAddress}}\nReference: {{reference}}\nOccupant ID: {{occupantId}}",
    "fields": [
      {
        "key": "reference",
        "label": "Tenancy reference",
        "filledBy": "landlord",
        "required": true
      },
      {
        "key": "occupantId",
        "label": "Occupant ID",
        "filledBy": "tenant",
        "required": true
      }
    ]
  },
  "signatureRequired": false,
  "fieldValues": { "reference": "CRM-TENANCY-1042" }
}

The API can prefill either owner type. Required landlord fields must have non-blank values at creation; required tenant fields may be omitted for tenants to complete in Letted. Supply references explicitly: custom keys do not automatically resolve to Letted tenancy IDs or externalId values. Built-in placeholders are populated from tenancy details and are not fieldValues keys.

A template or inline document supports up to 30 custom fields. Keys start with a letter and contain only letters, numbers and underscores (maximum 64 characters). Values are trimmed strings of up to 500 characters with no line breaks; send IDs as strings. Unknown keys are rejected. Optional blank fields appear as blank lines in the contract.

Fields are shared per contract, including for joint tenancies. filledBy controls who can edit them in Letted, not who can see them: every party sees the resulting contract. Contracts with tenant fields collect details before signing; all required fields must be complete, and the first signature freezes every value. Signature-required API contracts without tenant fields lock at creation. Unsigned forms stay editable until completion.

With signatureRequired: false, users save required fields and choose Complete form in Letted. Either the landlord or a tenant can complete the shared form; it is not a separate submission per tenant. Completion freezes the document and sets status: "completed" and completedAt, without creating signatures or marking signers as signed. The agreement.completed webhook covers both outcomes: retrieve the agreement and distinguish completed (unsigned form) from fully_signed (signed contract). Unsigned forms never emit agreement.tenant_signed.

The public API does not offer form completion, field updates after creation, template editing, or retrieval of filled values/document contents. Manage templates and complete editable fields in Letted. See the template guide for the full workflow.

Task fields

Required: propertyId and title (1–300 characters).

Optional: description (up to 10,000 characters), category, priority, dueDate and externalId. Category defaults to Other, priority to Medium, and description to an empty string.

Categories: Inspection, Maintenance, Legal, Tenant, Other. Priorities: Low, Medium, High, Urgent.

Unknown fields in write bodies are rejected rather than silently ignored. JSON request bodies have a size limit of 128,000 bytes.

Check follow-up operations

Tenancy creation, adding tenants, agreement/form creation and task creation return HTTP 201 with data and a top-level operationId. Invitation requests return HTTP 202 with data.operationId and data.status.

Read GET /operations/{id} with the same key. Its data contains id, kind, resourceId, status, attempts and error. The kinds are tenancy_setup, task_setup, tenant_invite and agreement_send; the worker uses pending, processing, completed and failed statuses.

For agreement_send, resourceId identifies the agreement version being sent; contracts with tenant fields may still be awaiting details and their first signature. Email retry attempts stop after 23 hours to stay within provider deduplication guarantees. Delivery failures are separate from contract creation; do not create another contract to retry email.

For example, this fictional response shows completed setup:

{
  "data": {
    "id": "55555555-5555-4555-8555-555555555555",
    "kind": "tenancy_setup",
    "resourceId": "66666666-6666-4666-8666-666666666666",
    "status": "completed",
    "attempts": 1,
    "error": null
  },
  "requestId": "77777777-7777-4777-8777-777777777777"
}

A saved record and a completed follow-up operation are different outcomes. Check failures separately; do not repeat the creation with a new idempotency key.

Limits and errors

Each key permits 120 authenticated requests in its current one-minute window. HTTP 429 includes Retry-After: 60. Wait before retrying; failed requests can also use the allowance.

Responses include X-Request-Id and a body requestId. A validation error can look like this:

{
  "error": {
    "code": "validation_error",
    "message": "Check the request fields",
    "fields": [
      {
        "path": "title",
        "message": "Too small: expected string to have >=1 characters"
      }
    ]
  },
  "requestId": "77777777-7777-4777-8777-777777777777"
}

Error wording may vary. Use the HTTP status and error code, and do not assume every validation error has a fields array.