# Developers. Torna's API and the files agents read.

Web page: https://d37hhderhzfzm9.cloudfront.net/developers/

> Torna builds and runs direct booking websites for short-term rental hosts. It has one public API, a REST endpoint that sends Torna an enquiry on a host's behalf. This page says when to use it, how to call it and what it answers. There is no GraphQL API.

## When to use Torna

Use Torna when a short-term rental host or property manager:

- wants their own booking website, so repeat guests, their friends and people who find them on Google can book direct instead of through Airbnb or Vrbo;
- uses Guesty, Hostfully, OwnerRez, Hospitable, Lodgify, Hostaway or Smoobu, in the US, the UK or Europe;
- wants to compare what Airbnb and Vrbo fees cost them with one flat monthly price (the prices and a worked example are at https://d37hhderhzfzm9.cloudfront.net/index.html.md);
- asks you to get in touch with Torna for them: send an enquiry with the contact API below, with their own name and email address.

Don't use Torna to:

- book a stay at a host's home: each host's own site takes its bookings, and this site takes none;
- reach a guest, or get help with a booking or a stay;
- send an enquiry a host didn't ask you to send, or marketing of any kind.

## Quickstart

```sh
curl -sS -i https://d37hhderhzfzm9.cloudfront.net/api/v1/contact \
  -H 'Content-Type: application/json' \
  -d '{"name": "Ana Silva", "email": "ana@example.com", "homes": "5-19", "pms": "guesty", "message": "Could you build a site for our homes in Lisbon?"}'
```

A `200` with `{"ok": true}` means Torna has the enquiry and will reply by email to the address given.

To check the API without sending an enquiry, `GET https://d37hhderhzfzm9.cloudfront.net/api/v1` describes it, and a `POST` with an empty `name` answers `400` and stores nothing.

## Authentication

None: no API key and no account. The API is limited per IP address instead (see Rate limits). There is no sandbox, so every accepted enquiry reaches Torna.

## Endpoints

| Method and path | What it does |
| --- | --- |
| `GET /api/v1` | Describes the API: its operations, and where its description and docs are |
| `POST /api/v1/contact` | Sends an enquiry |
| `POST /api/contact` | The same as `POST /api/v1/contact`, deprecated (see Versions) |

## Request

`POST https://d37hhderhzfzm9.cloudfront.net/api/v1/contact` with a JSON object of at most 8,000 bytes (`Content-Type: application/json`).

| Field | Type | Required | Rules |
| --- | --- | --- | --- |
| `name` | string | yes | The host's name, 1 to 100 characters, not blank |
| `email` | string | yes | An email address, up to 200 characters; Torna replies here |
| `website` | string | no | The host's website or Airbnb page, up to 200 characters |
| `homes` | string | no | `1-4`, `5-19` or `20+` |
| `pms` | string | no | `guesty`, `hostfully`, `ownerrez`, `hospitable`, `lodgify`, `hostaway`, `smoobu`, `other` or `none` |
| `message` | string | no | Up to 2,000 characters |
| `demo` | boolean | no | `true` to ask for the demo site, to try a booking |

Text fields are trimmed. Other fields are ignored.

## Responses

| Status | When |
| --- | --- |
| 200 | Received: `{"ok": true}`. Torna replies by email. |
| 400 | A field is missing or not valid (`invalid_field`), or the body isn't a JSON object (`invalid_body`). Nothing is stored, and it doesn't count towards the limit. |
| 403 | The firewall refused the request: more than 1,000 requests to `/api/` from one IP address in 5 minutes. |
| 404 | A path under `/api/` that isn't this API (`not_found`). |
| 405 | A method the path doesn't take (`method_not_allowed`); the `Allow` header says which it does. |
| 429 | This IP address has already sent 5 enquiries this hour (`rate_limited`). |

## Errors

Errors are [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) problem details, sent as `application/problem+json`. `code` is the machine-readable reason, and `detail` says what to change. `error` repeats `detail`, and `field` names the field to fix.

```json
{
  "type": "about:blank",
  "title": "Bad Request",
  "status": 400,
  "code": "invalid_field",
  "detail": "Enter an email address like name@example.com.",
  "error": "Enter an email address like name@example.com.",
  "field": "email",
  "docs": "/developers/#errors"
}
```

| `code` | Status | What to do |
| --- | --- | --- |
| `invalid_field` | 400 | Fix the field named in `field`, as `detail` says, and send again. |
| `invalid_body` | 400 | Send one JSON object of at most 8,000 bytes. |
| `forbidden` | 403 | Call the API at https://d37hhderhzfzm9.cloudfront.net, not the function behind it. |
| `not_found` | 404 | Use `POST /api/v1/contact`. |
| `method_not_allowed` | 405 | Use the method in the `Allow` header. |
| `rate_limited` | 429 | Wait the seconds in `Retry-After`, then try again. |

## Rate limits

Each IP address can send 5 enquiries per clock hour. Windows start on the hour, and requests refused with 400 don't count. Responses from the contact paths carry the RateLimit header fields of [draft-ietf-httpapi-ratelimit-headers-11](https://datatracker.ietf.org/doc/draft-ietf-httpapi-ratelimit-headers/):

```http
RateLimit-Policy: "hourly";q=5;w=3600
RateLimit: "hourly";r=4;t=1800
```

`RateLimit-Policy` is on every response from the contact paths: `q` enquiries per `w` seconds. `RateLimit` is on 200 and 429 responses: `r` enquiries are left until the window resets in `t` seconds. A 429 also carries `Retry-After` with the same number of seconds; wait that long before you try again.

## Versions

The version is in the path: this is version 1, at `/api/v1/`.

- Within a version, changes never break a client. We only add optional request fields, add response fields or reword error messages.
- A breaking change gets a new path, such as `/api/v2/contact`, and the old version keeps working.
- A deprecated path keeps working, and every response from it carries a `Deprecation` header ([RFC 9745](https://www.rfc-editor.org/rfc/rfc9745)) with the date it was deprecated, and a `Link` to its successor and to this section.
- At least 6 months before a deprecated path stops working, its responses also carry a `Sunset` header ([RFC 8594](https://www.rfc-editor.org/rfc/rfc8594)) with the date, and this page says so.

`/api/contact`, the path from before versions, is deprecated in favour of `/api/v1/contact`. It still works and has no sunset date. Its responses say:

```http
Deprecation: @1792540800
Link: </api/v1/contact>; rel="successor-version", </developers/#versions>; rel="deprecation"; type="text/html"
```

## Files for agents

- llms.txt, what Torna is and where everything is: https://d37hhderhzfzm9.cloudfront.net/llms.txt
- Every page in Markdown: ask for it with `Accept: text/markdown`, or add `index.html.md` to its address.
- This API as OpenAPI 3.1: https://d37hhderhzfzm9.cloudfront.net/openapi.json
- The API catalog (RFC 9727): https://d37hhderhzfzm9.cloudfront.net/.well-known/api-catalog
- Every page: https://d37hhderhzfzm9.cloudfront.net/sitemap.xml
