{
  "openapi": "3.1.0",
  "info": {
    "title": "Torna contact API",
    "version": "1.0.0",
    "summary": "Send Torna an enquiry for a short-term rental host.",
    "description": "The same as the contact form on Torna's website: Torna replies by email to the address given. Send only enquiries a host asked you to send, with their own name and email address.\n\nVersions: the version is in the path (/api/v1/). Within a version, changes never break a client: new optional fields, new response fields and new error wording only. A breaking change gets a new path (/api/v2/). A deprecated path keeps working, and its responses carry a Deprecation header (RFC 9745) and a Link to its successor; at least 6 months before it stops working they also carry a Sunset header (RFC 8594) with the date. /api/contact is deprecated in favour of /api/v1/contact and has no sunset date.\n\nErrors are RFC 9457 problem details (application/problem+json) with a machine-readable code. No API key is needed. This is Torna's only API. There is no GraphQL API.",
    "contact": {
      "name": "Torna",
      "url": "https://d37hhderhzfzm9.cloudfront.net/#contact"
    }
  },
  "servers": [
    {
      "url": "https://d37hhderhzfzm9.cloudfront.net"
    }
  ],
  "externalDocs": {
    "description": "This API's docs, with when to use it",
    "url": "https://d37hhderhzfzm9.cloudfront.net/developers/"
  },
  "paths": {
    "/api/v1": {
      "get": {
        "operationId": "getApiIndex",
        "summary": "Describe the API",
        "description": "What this API offers and where its description and docs are. Not rate-limited by the API, only by the firewall.",
        "responses": {
          "200": {
            "description": "The API's name, version, operations and links.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Index"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/contact": {
      "post": {
        "operationId": "sendEnquiry",
        "summary": "Send an enquiry",
        "description": "Takes a JSON object of at most 8,000 bytes. Each IP address can send 5 enquiries per clock hour; requests refused with 400 don't count.",
        "requestBody": {
          "$ref": "#/components/requestBodies/Enquiry"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/Received"
          },
          "400": {
            "$ref": "#/components/responses/BadField"
          },
          "403": {
            "$ref": "#/components/responses/Firewall"
          },
          "405": {
            "$ref": "#/components/responses/PostOnly"
          },
          "429": {
            "$ref": "#/components/responses/TooMany"
          }
        }
      }
    },
    "/api/contact": {
      "post": {
        "operationId": "sendEnquiryUnversioned",
        "summary": "Send an enquiry (old path)",
        "description": "The same as POST /api/v1/contact, from before the API had a version in its path. It keeps working, and each response says it is deprecated; use /api/v1/contact instead.",
        "deprecated": true,
        "requestBody": {
          "$ref": "#/components/requestBodies/Enquiry"
        },
        "responses": {
          "200": {
            "description": "Received. Torna replies by email.",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "Deprecation": {
                "$ref": "#/components/headers/Deprecation"
              },
              "Link": {
                "$ref": "#/components/headers/Link"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                }
              }
            }
          },
          "400": {
            "description": "A field is missing or not valid, named in `field`. Nothing was stored, and the request doesn't count towards the limit.",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "Deprecation": {
                "$ref": "#/components/headers/Deprecation"
              },
              "Link": {
                "$ref": "#/components/headers/Link"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "Refused by the firewall: more than 1,000 requests to /api/ from one IP address in 5 minutes."
          },
          "405": {
            "description": "Only POST is accepted.",
            "headers": {
              "Allow": {
                "$ref": "#/components/headers/Allow"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "Deprecation": {
                "$ref": "#/components/headers/Deprecation"
              },
              "Link": {
                "$ref": "#/components/headers/Link"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "This IP address has already sent 5 enquiries this hour.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "Deprecation": {
                "$ref": "#/components/headers/Deprecation"
              },
              "Link": {
                "$ref": "#/components/headers/Link"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Enquiry": {
        "type": "object",
        "required": [
          "name",
          "email"
        ],
        "additionalProperties": true,
        "description": "Text fields are trimmed. Other fields are ignored.",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100,
            "description": "The host's name; not blank."
          },
          "email": {
            "type": "string",
            "format": "email",
            "maxLength": 200,
            "description": "Where Torna replies."
          },
          "website": {
            "type": "string",
            "maxLength": 200,
            "description": "The host's website or Airbnb page."
          },
          "homes": {
            "type": "string",
            "enum": [
              "",
              "1-4",
              "5-19",
              "20+"
            ],
            "description": "How many homes the host has."
          },
          "pms": {
            "type": "string",
            "enum": [
              "",
              "guesty",
              "hostfully",
              "ownerrez",
              "hospitable",
              "lodgify",
              "hostaway",
              "smoobu",
              "other",
              "none"
            ],
            "description": "The host's booking system: other for one not listed, none for no booking system."
          },
          "message": {
            "type": "string",
            "maxLength": 2000,
            "description": "What the host would like to ask."
          },
          "demo": {
            "type": "boolean",
            "default": false,
            "description": "true to ask for the demo site, to try a booking."
          }
        }
      },
      "Ok": {
        "type": "object",
        "required": [
          "ok"
        ],
        "properties": {
          "ok": {
            "const": true
          }
        }
      },
      "Problem": {
        "type": "object",
        "description": "An error, as RFC 9457 problem details. code is the machine-readable reason; detail says what to change. error repeats detail and field names the field to fix, as in v1's first error shape.",
        "required": [
          "type",
          "title",
          "status",
          "detail",
          "code",
          "error"
        ],
        "properties": {
          "type": {
            "type": "string",
            "const": "about:blank"
          },
          "title": {
            "type": "string",
            "description": "The HTTP status phrase, such as Bad Request."
          },
          "status": {
            "type": "integer"
          },
          "detail": {
            "type": "string",
            "description": "What went wrong and what to do, in plain words."
          },
          "code": {
            "type": "string",
            "enum": [
              "invalid_field",
              "invalid_body",
              "forbidden",
              "not_found",
              "method_not_allowed",
              "rate_limited"
            ]
          },
          "error": {
            "type": "string",
            "description": "The same as detail."
          },
          "field": {
            "type": "string",
            "description": "On invalid_field: the field to fix. On invalid_body: form."
          },
          "docs": {
            "type": "string",
            "description": "Where the errors are explained: /developers/#errors."
          }
        }
      },
      "Index": {
        "type": "object",
        "required": [
          "name",
          "version",
          "endpoints",
          "openapi",
          "docs"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "version": {
            "type": "string"
          },
          "endpoints": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "method": {
                  "type": "string"
                },
                "path": {
                  "type": "string"
                },
                "operationId": {
                  "type": "string"
                },
                "description": {
                  "type": "string"
                }
              }
            }
          },
          "openapi": {
            "type": "string"
          },
          "docs": {
            "type": "string"
          },
          "catalog": {
            "type": "string"
          }
        }
      }
    },
    "requestBodies": {
      "Enquiry": {
        "required": true,
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Enquiry"
            },
            "example": {
              "name": "Ana Silva",
              "email": "ana@example.com",
              "homes": "5-19",
              "pms": "guesty",
              "website": "casalume.example",
              "message": "Could you build a site for our homes in Lisbon?",
              "demo": true
            }
          }
        }
      }
    },
    "responses": {
      "Received": {
        "description": "Received. Torna replies by email.",
        "headers": {
          "RateLimit": {
            "$ref": "#/components/headers/RateLimit"
          },
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimit-Policy"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Ok"
            }
          }
        }
      },
      "BadField": {
        "description": "A field is missing or not valid (code invalid_field, with the field named in field), or the body isn't a JSON object (invalid_body). Nothing was stored, and the request doesn't count towards the limit.",
        "headers": {
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimit-Policy"
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "Firewall": {
        "description": "Refused by the firewall: more than 1,000 requests to /api/ from one IP address in 5 minutes."
      },
      "PostOnly": {
        "description": "Only POST is accepted.",
        "headers": {
          "Allow": {
            "$ref": "#/components/headers/Allow"
          },
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimit-Policy"
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "TooMany": {
        "description": "This IP address has already sent 5 enquiries this hour.",
        "headers": {
          "Retry-After": {
            "$ref": "#/components/headers/Retry-After"
          },
          "RateLimit": {
            "$ref": "#/components/headers/RateLimit"
          },
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimit-Policy"
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      }
    },
    "headers": {
      "RateLimit-Policy": {
        "description": "The limit, as in draft-ietf-httpapi-ratelimit-headers-11: q enquiries per w seconds, per IP address. Windows start on the hour.",
        "schema": {
          "type": "string",
          "examples": [
            "\"hourly\";q=5;w=3600"
          ]
        }
      },
      "RateLimit": {
        "description": "What is left of the limit: r enquiries until the window resets in t seconds.",
        "schema": {
          "type": "string",
          "examples": [
            "\"hourly\";r=4;t=1800"
          ]
        }
      },
      "Retry-After": {
        "description": "Seconds until the next hour starts and the limit resets.",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 3600
        }
      },
      "Allow": {
        "description": "The methods this path accepts.",
        "schema": {
          "type": "string",
          "const": "POST"
        }
      },
      "Deprecation": {
        "description": "On a deprecated path only (RFC 9745): when the path was deprecated, as @ and Unix seconds.",
        "schema": {
          "type": "string",
          "examples": [
            "@1792540800"
          ]
        }
      },
      "Sunset": {
        "description": "Sent at least 6 months before a deprecated path stops working (RFC 8594): the HTTP date it stops. No path has one yet.",
        "schema": {
          "type": "string",
          "examples": [
            "Wed, 21 Apr 2027 00:00:00 GMT"
          ]
        }
      },
      "Link": {
        "description": "On a deprecated path: its successor-version and the deprecation policy.",
        "schema": {
          "type": "string",
          "examples": [
            "</api/v1/contact>; rel=\"successor-version\", </developers/#versions>; rel=\"deprecation\"; type=\"text/html\""
          ]
        }
      }
    }
  }
}
