Contracts
Define what the contract says by combining a template with a set of house rules, and download the PDF signed by the party along with its evidence trail.
The cycle has two halves. First you define what the contract says — the legal articles, in a template, and the rules of the place, in a house rules set — and choose which combination each property or each booking uses. Then, once the party signs, Kinapi generates the signed contract for that booking and you can download it. All over the API.
A contract is two pieces
The document your guest signs is built from two things that change for different reasons. The legal articles (subject of the contract, liability, governing law) are much the same across your whole portfolio; the house rules change from one property to the next — the villa with a pool does not share them with the city-centre flat.
That is why they are two separate resources: templates under /contract-templates hold the articles, and sets under /house-rules hold the rules. They are combined when each contract is generated, so M templates and N rule sets give you all M × N combinations with nothing duplicated. House rules used to live inside the template: changing a single rule for one apartment meant duplicating the entire template — legal articles included — and then keeping the same legal text in several copies that drift apart on their own.
What you already have keeps working. Rules written inside a template (houseRules) or inside a property are still printed whenever a booking resolves to no set at all. Nothing needs migrating for your current contracts to come out exactly as they did yesterday.
Where this lives in the panel. Terms and rules share one section, Documents, with two tabs: Contracts for the legal text, which is one for your whole portfolio, and House rules for the rule sets, which change from house to house. They sit together because they combine into a single document; split across the menu they looked unrelated.
Why several templates
One account can run accommodations with different legal conditions — an apartment block and a country house do not always sign the same thing. Each template is created once and assigned by id to as many properties as use it (contractTemplateId in /properties). A property with none assigned uses the one marked isDefault for the tenant.
If the only difference between two properties is the rules, you do not need a second template: create another house rules set and assign that instead. Templates only multiply when the legal articles change.
The template object
{
"id": "8a2e1f0c-…",
"name": "Premium villas",
"isDefault": true,
"bodyMode": "clauses",
"showOwnerInfo": true,
"showOwnerDocument": true,
"showPropertyInfo": true,
"houseRules": [
{ "es": "No se permiten fiestas ni eventos.", "en": "No parties or events." }
],
"legalClauses": [
{ "es": "El huésped se compromete a respetar el aforo máximo indicado.", "en": "…" }
],
"createdAt": "2026-07-01T09:00:00.000Z",
"updatedAt": "2026-07-19T10:00:00.000Z"
}Each clause is bilingual: es is required, en optional (the generated contract always carries both languages — if en is missing, that clause appears in Spanish only).
The template’s houseRules field is the older model and is kept for compatibility: it is only printed when the booking resolves to no house rules set. On a fresh setup, leave the field alone and write the rules in their own resource.
Pasting a contract you already have
If your contract is already written, there is no need to chop it into clauses. Set bodyMode: "document" and send the whole text in body, one entry per language.
curl -X PATCH https://api.kinapi.es/api/v1/contract-templates/8a2e1f0c-… -H "ocp-apim-subscription-key: YOUR_API_KEY" -H "content-type: application/json" -d '{
"bodyMode": "document",
"body": {
"es": "# CONTRATO DE ARRENDAMIENTO
## Artículo 1. Objeto
Este contrato regula el **uso turístico**…",
"nl": "# HUUROVEREENKOMST
## Artikel 1. Voorwerp
Deze overeenkomst regelt het **toeristisch gebruik**…"
}
}'The accepted markup is deliberately small, because anything accepted has to be drawable in the PDF: # ## ### for chapter, article and sub-section headings; - and 1. for lists; **bold** and *italic*. There are no tables, images, links or multi-level numbering: an “Article 3.2” is written as a heading.
List numbering is recalculated at print time, so you can write every item as 1. and they will come out 1, 2, 3. Inserting an article in the middle does not force you to renumber the rest.
The contract is always printed in two columns: the language of the booking and Spanish. The property is in Spain and the contract itself submits to Spanish courts, so the Spanish version cannot be removed. If the booking is already in Spanish, the second column is English.
A language you have not written falls back to Spanish in full, never halfway: nobody signs a contract with half the articles in one language and half in another. And the legal conditions (filing with SES Hospedajes, data protection and governing law) are appended at the end in both modes.
In document mode the text you paste usually carries its own rules inside it, so the rules of the older model — those written inside the template or inside the property — are not appended. A house rules set is printed, in its own section after the body, whenever the booking resolves to one: its own, the property’s or the tenant default. That is exactly the “articles × rules” combination the split exists to allow.
The contract is issued in the booking’s language, and that comes from a chain of three: the locale you send in POST /checkins, failing that the communicationLocale of the accommodation, and failing that Spanish. It is the same chain that decides the guest form and the emails they receive: one setting governs all three, so you can never invite someone in one language and have them sign in another.
With contractTemplateId in that same call you can pick a different template per booking, if you prefer one per language rather than a single multilingual document.
The flag the guest taps on their own link changes the language of the form while they register, but not that of the contract: that stays the booking’s. Whoever signs cannot change the language of the document they are signing, neither by accident nor on purpose.
showOwnerInfo, showOwnerDocument and showPropertyInfo control which sections of the PDF appear — not which data is used (that always comes from the assigned property and owner). For instance, to avoid revealing the owner’s tax number on the contract the guest signs, leave showOwnerInfo at true but set showOwnerDocument to false.
Which template a contract uses
From most specific to most general, the first one that exists wins:
- The booking’s template (
contractTemplateIdon the check-in). - The property’s template (
contractTemplateIdon the property). - The tenant default (
isDefault). - Kinapi’s standard clauses, for an account that has not created any template yet.
An id pointing at a deleted template falls back to the tenant default, not to the standard clauses. If you delete a template that some booking or property still referenced, those contracts move to your own default articles — they used to come out with Kinapi’s generic text, with no warning at all.
Only one id is looked up: the booking’s if there is one, otherwise the property’s. A booking referencing a deleted template goes straight to the tenant default; it does not fall back to the property’s template.
The signed contract arrives by email
When the party finishes signing, the PDF goes out by email to every one of the people who signed it, attached and carrying the accommodation's branding. There is nothing to switch on or ask for: it happens whenever the signer left an email address. The download link keeps working exactly as before — this is an addition, it replaces nothing.
It goes out as one email per person, not one with everybody in the To field. The guests on a booking are separate people, and putting them together in the header hands each one's address to all the others, which none of them agreed to.
It is sent once per booking, and sending it never blocks the signature: if the email fails, the guest has already signed and the contract is still downloadable from their link. A signature never depends on an email provider answering.
List templates
/contract-templatescurl https://api.kinapi.es/api/v1/contract-templates \ -H "ocp-apim-subscription-key: YOUR_API_KEY"
Create a template
/contract-templates| Field | Type | Description |
|---|---|---|
namerequired | string | Name of the template. |
isDefault | boolean | Mark it as the tenant default. |
showOwnerInfo | boolean | Show the owner/company details section in the PDF. true by default. |
showOwnerDocument | boolean | Show the owner’s tax number inside that section. true by default; only applies if showOwnerInfo is on. |
showPropertyInfo | boolean | Show the property details section (accommodation and address). true by default. |
showTouristLicense | boolean | Print the tourist licence inside that section. true by default; only applies when showPropertyInfo is on. Set it to false on a seasonal-lease template: the property may well hold a licence, and showing it there misstates which legal regime the signed document belongs to. If the property has no licence, the line never prints anyway. |
titulo | {es, en} | Heading of the PDF. Absent ⇒ “CONTRATO DE ALQUILER TURÍSTICO / Tourist Rental Contract”. Change it when your stays are not tourist lets — for example “CONTRATO DE ARRENDAMIENTO TEMPORAL” for a seasonal lease under Title III of the LAU. |
houseRules | [{es, en?}] | Rules written inside the template (older model). If omitted, it is created with Kinapi’s standard house rules. They are only printed when the booking resolves to no set from /house-rules. |
legalClauses | [{es, en?}] | If omitted, it is created with Kinapi’s standard legal clauses. |
bodyMode | 'clauses' | 'document' | How the body is written. 'clauses' (the default) uses houseRules; 'document' uses body. The clauses are kept when you switch modes. |
body | {es, en?, de?, fr?, nl?, it?, pt?} | Document mode only: the whole contract, one text per language. `es` is REQUIRED (the PDF always carries Spanish). It is merged per language — sending only `nl` does not wipe the stored `es` — and an empty string deletes that language. Max. 60,000 characters per language. |
curl -X POST https://api.kinapi.es/api/v1/contract-templates \
-H "ocp-apim-subscription-key: YOUR_API_KEY" \
-H "content-type: application/json" \
-d '{ "name": "Premium villas", "isDefault": true }'Creating without houseRules/legalClauses is the quickest way to start: you get Kinapi’s standard clauses and then edit them with a PATCH.
Read a template
/contract-templates/{id}Update a template
/contract-templates/{id}Partial update: only the fields you send change. To edit clauses, send the whole houseRules or legalClauses list — it replaces the previous one, it does not merge clause by clause.
curl -X PATCH https://api.kinapi.es/api/v1/contract-templates/8a2e1f0c-… \
-H "ocp-apim-subscription-key: YOUR_API_KEY" \
-H "content-type: application/json" \
-d '{ "houseRules": [{ "es": "No se admiten mascotas.", "en": "No pets." }] }'Delete a template
/contract-templates/{id}Properties and bookings that had it assigned fall back to the tenant default (or to Kinapi’s standard clauses if there is no default either). The orphaned contractTemplateId stays stored — nobody cleans the reference up — but when the contract is generated no other assigned template is looked for: it goes straight to the default, even for a booking on a property that has one of its own.
Preview a template
/contract-templates/{id}/previewReturns a sample PDF built with this template — clauses or document, depending on its bodyMode — over fictional data. It lets you check how the contract turns out while you are assembling it over the API, without creating a real check-in or waiting for anyone to sign.
It accepts ?locale= so you can see it in a given booking’s language. It applies exactly the same rules as the real contract: what you see here is what your guests will sign.
It shows the template on its own: its articles and, in clauses mode, the rules written inside it. Assigned house rules sets are not combined here, because that combination depends on the property and the booking, and a preview has neither.
curl -OJ -X POST "https://api.kinapi.es/api/v1/contract-templates/8a2e1f0c-…/preview?locale=nl" \ -H "ocp-apim-subscription-key: YOUR_API_KEY"
House rules
A house rules set is a document of its own, a sibling of the contract template rather than a section inside it. You create and edit sets in the House rules area of the dashboard or over the API at /house-rules, and they are combined with the legal articles when each contract is generated.
A set is written in one of two ways, the same two the contract body offers: clauses (a bilingual list, rule by rule) or document (the text already written out, one per language, with the same markup as a pasted contract). The clauses are kept when you switch to document mode: going back never costs you work already done.
The house rules object
{
"id": "c41b90a7-…",
"name": "Villas with a pool",
"isDefault": false,
"bodyMode": "clauses",
"clauses": [
{ "es": "La piscina cierra a las 22:00.", "en": "The pool closes at 22:00." },
{ "es": "No se admiten mascotas.", "en": "No pets allowed." }
],
"hasText": true,
"createdAt": "2026-08-01T09:00:00.000Z",
"updatedAt": "2026-08-12T10:00:00.000Z"
}In document mode the object also carries body, one entry per language, and clauses stays there as a backup. hasText is a computed field: it tells you whether the set has actual text in the mode it is written in.
A set with no text appears in no contract. It is skipped while resolving and the next candidate is used, so assigning it raises no error but prints nothing either. Check hasText before assuming a freshly created set is already printing.
Which rules a contract prints
From most specific to most general, the first one with actual text wins:
- The booking’s set (
houseRulesIdon the check-in). - The property’s set (assigned from the dashboard, on the property itself).
- The tenant default set (
isDefault). - The rules of the older model: those written inside the property if it has any, and otherwise those inside the contract template.
- Kinapi’s standard house rules, for an account that has not created any template yet.
An id that no longer exists does not break the chain: if you delete a set an old booking still referenced, that contract moves down one step instead of ending up with no rules. The same goes for a set that exists but is empty. And a template whose rules list you emptied on purpose prints no rules section at all: emptying it is an instruction, not a gap.
The rules are printed in their own section, NORMAS DE LA CASA / HOUSE RULES, right before the legal conditions. In clauses mode they come out numbered and bilingual; in document mode, in the booking’s language with Spanish underneath, just like the contract body.
With the template in document mode the last two steps never get printed: the text you pasted already carries its own rules, so only the first three add a section — a set from the booking, from the property, or the tenant default.
Assigning a set to a booking
With houseRulesId in POST /checkins you choose, in the same call that creates the booking, which articles and which rules make up the contract about to be signed — without needing a template for every crossing of the two axes.
curl -X POST https://api.kinapi.es/api/v1/checkins \
-H "ocp-apim-subscription-key: YOUR_API_KEY" \
-H "content-type: application/json" \
-d '{
"reservationId": "R-2026-0412",
"listingId": "ALJ-3F9K2P",
"confirmationCode": "HMXK4L",
"checkInDate": "2026-09-01",
"checkOutDate": "2026-09-08",
"numGuests": 4,
"contractTemplateId": "8a2e1f0c-…",
"houseRulesId": "c41b90a7-…"
}'You can also change it later with PATCH /checkins/{reservationId}. An empty string unassigns the booking’s set and hands control back to the property. A contract that is already signed is never regenerated — it keeps the rules it was signed with, its timestamp and its immutable copy — so the change only affects what gets signed from then on.
Each property’s set is assigned from the dashboard, on the property itself: that is configuration you set once and leave alone, whereas the booking is what your PMS decides call by call.
List house rules sets
/house-rulesReturns { "results": [...] } with every set in the tenant, ordered by name.
curl https://api.kinapi.es/api/v1/house-rules \ -H "ocp-apim-subscription-key: YOUR_API_KEY"
Create a house rules set
/house-rules| Field | Type | Description |
|---|---|---|
namerequired | string | The name you pick it by in the dashboard and the API ("Villas with a pool"). |
id | string | If sent, that set is updated instead of creating another one. It lets you retry a create without ending up with six identical sets when the first call died after saving. |
isDefault | boolean | Mark it as the tenant default. There can only be one: marking this one unmarks the previous. |
clauses | [{es, en?}] | The rules, one per item. If omitted on create, the set starts with Kinapi’s standard house rules; on an update (with id) it keeps the ones it already had. |
bodyMode | 'clauses' | 'document' | How the text is written. 'clauses' (the default) uses the list; 'document' uses body. The clauses are kept when you switch modes. |
body | {es, en?, de?, fr?, nl?, it?, pt?} | Document mode only: the rules already written out, one text per language, with the same markup as the contract body. `es` is REQUIRED. It is merged per language and an empty string deletes that language. Max. 60,000 characters per language. |
curl -X POST https://api.kinapi.es/api/v1/house-rules \
-H "ocp-apim-subscription-key: YOUR_API_KEY" \
-H "content-type: application/json" \
-d '{
"name": "Villas with a pool",
"clauses": [
{ "es": "La piscina cierra a las 22:00.", "en": "The pool closes at 22:00." }
]
}'It answers 201 with the new set, or 200 if you sent an id and it was therefore an update. Keep the id: it is what you send later as houseRulesId.
With bodyMode: "document" you must send body.es with text in it. Without it the call returns 400: the contract is always printed in Spanish as well, and that column cannot be generated on its own.
Read a house rules set
/house-rules/{id}Returns the whole set, clauses included even in document mode. 404 if the id does not belong to this tenant.
Update a house rules set
/house-rules/{id}Partial update: only the fields you send change. The clauses list is sent whole and replaces the previous one; body is merged per language, so sending only Dutch does not wipe the Spanish already written — and an empty string deletes that language, which is how you remove a translation.
curl -X PATCH https://api.kinapi.es/api/v1/house-rules/c41b90a7-… \
-H "ocp-apim-subscription-key: YOUR_API_KEY" \
-H "content-type: application/json" \
-d '{ "isDefault": true, "clauses": [{ "es": "No se admiten mascotas.", "en": "No pets allowed." }] }'Delete a house rules set
/house-rules/{id}It can be deleted even while properties or bookings reference it: those ids are left orphaned and, when the contract is generated, resolution moves to the next step of the precedence. No old booking ends up without rules, and no PDF fails to generate.
Deleting the default set does change contracts for properties the call never names: nobody inherits that role, so every property without a set of its own starts printing the rules of the older model. Mark another set as isDefault before deleting it.
Download the signed contract
/checkins/{reservationId}/contractReturns the final PDF of a booking’s contract: two columns — the booking’s language and Spanish — with each guest’s signature embedded and a closing evidence record page (date and time of each signature, IP, device and the SHA-256 hash of each signature image).
curl -OJ https://api.kinapi.es/api/v1/checkins/R-2026-0412/contract \ -H "ocp-apim-subscription-key: YOUR_API_KEY"
It answers 404 until the party has completed the signing (check contract.signed on the check-in to know when it is available).
What the evidence guarantees
Every completed contract automatically produces a three-layer trail:
- Immutable copy (WORM): the exact PDF and its timestamp are archived in storage with a 3-year retention policy: they cannot be overwritten or deleted before it expires.
- RFC 3161 timestamp: the PDF’s SHA-256 hash is timestamped by an external timestamping authority. When the token arrives we check that it stamps that hash and answers our nonce; only then does it count. Any third party can verify it with
openssl ts(below). - Append-only log: document hash, timestamp token and, per signer, IP + device + time + the hash of their signature.
The guest can also download their own copy from their link — a copy outside your control and ours, which would strengthen the evidence in any dispute.
Reading and verifying the evidence
The evidence record of every signed contract — document hash, timestamp and signers — is available over the API, and the timestamp downloads exactly as the authority issued it: an RFC 3161 TimeStampResp in DER (extension .tsr).
/checkins/{reservationId}/evidence| Field | Type | Description |
|---|---|---|
id | string | Record id; use it to download the timestamp. |
createdAt | ISO 8601 | When the last guest completed the signing and the final PDF was generated. |
sha256 | hex | SHA-256 of the signed PDF. This is exactly what was timestamped. |
tsa.ok | boolean | true when a verified timestamp exists (hash and nonce checked on receipt). |
tsa.genTime | ISO 8601 | Instant certified by the timestamping authority. |
tsa.authority | string | URL of the timestamping authority. |
tsa.archived | boolean | true when the timestamp is also in immutable storage, next to the PDF. |
tsa.error | string | Why there is no timestamp, when there is none. Retried every hour. |
signers | array | Per signer: guestId, name, signedAt, ip, userAgent, signatureSha256 and uiLocale (language they saw the form in). |
/checkins/{reservationId}/evidence/{evidenceId}/tokenReturns the timestamp as binary (application/timestamp-reply). 404 with no_timestamp if the record has no timestamp yet.
curl -OJ https://api.kinapi.es/api/v1/checkins/R-2026-0412/evidence/EVIDENCE_ID/token \ -H "ocp-apim-subscription-key: YOUR_API_KEY"
# What it stamps, and when openssl ts -reply -in token.tsr -text # That it stamps THIS PDF and the authority's signature is valid openssl ts -verify -digest $(shasum -a 256 contract.pdf | cut -d' ' -f1) \ -in token.tsr -CAfile /etc/ssl/cert.pem
The authority is DigiCert (timestamp.digicert.com): external to Kinapi, with its certificate inside the token so it can be verified years from now. It is not an eIDAS qualified timestamp: admissible as evidence, without the legal presumption of accuracy a qualified provider carries.
Traveller report (the document you must keep)
/checkins/{reservationId}/parteNot to be confused with the contract: the traveller entry report is the document RD 933/2021 requires you to keep and produce on request. With no parameters it returns a ZIP with each guest’s report; with ?guestId=, that person’s PDF.
curl -OJ "https://api.kinapi.es/api/v1/checkins/R-2026-0412/parte" \ -H "ocp-apim-subscription-key: YOUR_API_KEY"