Check-ins
The core resource of the API: one check-in per booking, always identified by your reservationId.
The check-in object
{
"reservationId": "R-2026-0412",
"listingId": "38291",
"confirmationCode": "HMXK4L",
"source": "direct",
"checkInDate": "2026-08-14",
"checkOutDate": "2026-08-18",
"arrivalTime": "16:00",
"departureTime": "11:00",
"numGuestsExpected": 4,
"numGuestsConfirmed": null,
"leadGuest": { "name": "Jan de Vries", "email": "jan@example.com", "phone": "+31600000000" },
"maxGuests": 6,
"status": "in_progress",
"locale": "nl",
"contractTemplateId": "9f2a…",
"houseRulesId": "b7c1…",
"tipoPago": "TRANS",
"url": "https://checkinsas…/c/aGb3xKf9…",
"contract": { "signed": false, "signedAt": null },
"createdAt": "2026-07-19T10:12:00.000Z",
"updatedAt": "2026-07-19T11:40:00.000Z"
}The optional fields — maxGuests, returnUrl, contractTemplateId, houseRulesId, tipoPago, skipContract, manualSubmit and errorReason — only appear when the booking carries them. Their absence means the booking does not have them, not that they are unknown.
anonymizedAt only appears on a booking whose personal data was suppressed when its owner was offboarded: it is still listed, but it no longer has guests, a lead guest or files. See Exports and offboarding.
| Field | Type | Description |
|---|---|---|
pending | status | Created; no guest registered yet. |
in_progress | status | At least one guest registered. |
completed | status | Every guest registered. |
signed | status | Contract signed by the whole party. |
submitted | status | Traveller report sent to SES (batch assigned). |
confirmed | status | SES confirmed every communication. |
error | status | Needs attention — see errorReason. |
cancelled | status | Booking cancelled. |
Create a check-in
/checkins| Field | Type | Description |
|---|---|---|
reservationIdrequired | string | The booking id in your system. It is the public identifier of the check-in. |
listingIdrequired | string | Listing id — a property with that listingId must exist. If none does, the booking is still created, in the error status, and the response carries a warning with the reason: nothing is lost silently. |
confirmationCoderequired | string | The confirmation code the guest sees. |
checkInDaterequired | YYYY-MM-DD | Arrival date. |
checkOutDaterequired | YYYY-MM-DD | Departure date. |
numGuestsrequired | integer 1-50 | Expected number of guests (the party can adjust it). |
arrivalTime | hh:mm | Check-in time (16:00 by default). |
departureTime | hh:mm | Check-out time (11:00 by default). |
source | string | Booking channel: airbnb, booking, direct… Free text and informational: it shows in the panel and comes back in responses, but is not sent to SES. Without it, the booking is left as unknown. |
leadGuest | object | { name, email, phone } of the lead guest, to prefill their registration. |
locale | string | Language of the form, the contract and the emails to the guest: es, en, de, fr, nl, it or pt. Regional variants are accepted (nl-NL → nl). If you do NOT send it, the property's own default language is used, and Spanish if it has none. What you send here overrides the property, because it is about one specific guest. A language we do not support falls back to Spanish WITHOUT an error — check the locale in the response to see which one was applied. |
returnUrl | string | https URL the guest goes back to when they finish the check-in, or when they choose to leave part-way. They go back by TAPPING a button, never on their own: that way they can stay to review what they sent, and opening their link again does not bounce them out of it. Lets you embed the registration inside your own flow. https only and no embedded credentials; an invalid URL returns 400. We check the SHAPE, not that it works: send it complete, including any trailing token or id, because a well-formed but incomplete URL is accepted and fails at the very end, once the guest has already finished. |
maxGuests | integer 1-50 | The property’s real capacity. Guests can adjust the group size from their link, and this is the ceiling they cannot exceed. If the number sent was too high they can always LOWER it, even above the cap. Without it only the global limit applies. |
contractTemplateId | string | Contract template for this booking. It takes precedence over the one assigned to the property; with neither, the tenant default is used. |
houseRulesId | string | House rules set for this booking. It is the other axis of the contract: it combines with contractTemplateId, so you never need one template per pairing of legal clauses and rules. It takes precedence over the set assigned to the property. |
tipoPago | string | How THIS stay was paid for: a code from the SES TIPO_PAGO catalogue (PLATF, TRANS, EFECT…). It overrides the one on the property and the one on the SES credentials. Send it only when that booking was collected differently from the property’s usual method. |
skipContract | boolean | true = this booking carries no terms of your own: the document comes out with the house rules and the legal conditions, nothing else. For stays that do not need a contract. The legal conditions cannot be removed: they carry the notice that data is filed with the Ministry and the data-protection information. |
manualSubmit | boolean | true = this booking’s report does NOT go out in the nightly submission to SES.Hospedajes, nor is it filed again on its own if its dates change. It is reported when you ask for it: from the panel with “Send manually”, or by calling POST /checkins/{reservationId}/ses. For stays you want to review first. The legal deadline for the report still applies. |
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": "38291",
"confirmationCode": "HMXK4L",
"checkInDate": "2026-08-14",
"checkOutDate": "2026-08-18",
"numGuests": 4
}'{
"url": "https://checkinsas…/c/aGb3xKf9…",
"status": "pending",
"created": true,
"locale": "es",
"contractTemplateId": "9f2a…",
"houseRulesId": "b7c1…",
"tipoPago": "TRANS"
}contractTemplateId, houseRulesId and tipoPago only come back when the booking has them stored: that is how you confirm they were saved. All three — along with skipContract and manualSubmit — also come back in the check-in object (GET and PATCH), so whatever you change later can be read back without holding on to the response from the creation call.
Idempotent: if the check-in already exists, it answers 200 with the existing one (created: false) and the same URL. Retry freely. The second call does not modify the booking that was already there: to change its dates, template, rules or payment method, use PATCH.
For returnUrl we check the shape, not that it works. That it is https and carries no credentials inside; that is where it ends. We never visit it or verify that it exists, because at the moment the booking is created it may not exist yet, and because opening connections to addresses dictated from outside is a risk that is not worth what it buys.
The practical consequence: a well-formed but incomplete URL is accepted without complaint and fails at the end, when the guest has already finished, taps to go back and lands on a 404. This happens with addresses that carry a token or an id in the last segment and are assembled piece by piece — …/booking/R-2026-0412 instead of …/booking/R-2026-0412/f9K2xQ. Send it whole, token included, and test it by pasting it into a browser before you wire it up.
Contract and house rules: two axes that combine
The legal clauses are the same across almost the whole portfolio; what changes from one home to the next are the rules — pool and barbecue here, lift and quiet hours there. That is why house rules are an entity of their own rather than a section inside the contract template: every booking picks its own pairing with contractTemplateId (the clauses) and houseRulesId (the rules).
M templates and N rule sets therefore give you M × N contracts without duplicating a single paragraph. With the rules living inside the template, changing one rule for one apartment meant duplicating the whole template — legal clauses included — and then keeping the same legal text in copies that drift apart on their own. Rule sets are created and listed at /v1/house-rules, and a property is given its own set from the panel; see Contracts and Properties.
| Field | Type | Description |
|---|---|---|
houseRulesId on the booking | 1st | What you send in POST or PATCH /checkins. The only one that can be decided call by call, with the booking already in front of you. |
houseRulesId on the property | 2nd | The set assigned to the property, either from its panel record (Property details) or over the API: /v1/properties accepts it on creation and on PATCH, and returns it in the property object. It covers every booking of that property that does not carry its own. |
the tenant default set | 3rd | The one flagged isDefault in /v1/house-rules. It covers every property with no set of its own. |
inherited rules | 4th | With no set to apply, the property’s own rules (the deprecated houseRules field) or, when those are empty, the ones written inside the contract template. Failing all of the above, Kinapi’s factory rules. |
A houseRulesId pointing at a deleted set, or at one left with no text in any language, does not break the chain: resolution carries on with the next candidate. A deletion months later cannot strip the rules from bookings that already referenced it.
Payment method for the booking
tipoPago is the code from the Ministry’s TIPO_PAGO catalogue that travels in the traveller report. It belongs to the booking, not to the property: the same house is paid through the platform one week and by bank transfer the next, and what SES asks is how that stay was paid for.
| Field | Type | Description |
|---|---|---|
tipoPago on the booking | 1st | What you send in POST or PATCH /checkins. Normally you leave it out and let the property decide. |
ses.tipoPago on the property | 2nd | How that property is usually paid for. |
tipoPago on the SES credentials | 3rd | The landlord default. It is required when the credentials are created, so the cascade always ends in a value and no report goes out without a payment method. |
Valid codes: EFECT, TARJT, PLATF, TRANS, MOVIL, DESTI, TREG and OTRO — the table with what each one means is in SES credentials. The value is read when the report is built, not when the booking is created: a PATCH before submission changes what gets filed. After that, changing only the payment method does not redo what was filed; changing the dates does, and the new filing carries the current value.
List check-ins
/checkins| Field | Type | Description |
|---|---|---|
from | YYYY-MM-DD | Arrival from (inclusive). |
to | YYYY-MM-DD | Arrival to (inclusive). |
status | string | Filter by status (see the status table). |
listingId | string | Only check-ins for that listing. |
ownerId | string | Only the bookings of this owner’s properties (id from /owners), including their deactivated properties and the bookings that came in under a channel id. An owner that does not exist returns 400 validation. |
limit | integer 1-200 | Page size (50 by default). |
offset | integer | Pagination offset. |
curl "https://api.kinapi.es/api/v1/checkins?from=2026-08-01&to=2026-08-31&status=confirmed" \ -H "ocp-apim-subscription-key: YOUR_API_KEY"
{
"results": [ { …check-in… }, … ],
"hasMore": false
}Read a check-in
/checkins/{reservationId}Returns the check-in with the guests array included (same shape as the Guests resource).
curl https://api.kinapi.es/api/v1/checkins/R-2026-0412 \ -H "ocp-apim-subscription-key: YOUR_API_KEY"
Update a check-in
/checkins/{reservationId}| Field | Type | Description |
|---|---|---|
checkInDate | YYYY-MM-DD | New arrival date. If the report has already been filed, Kinapi annuls it and files it again with the new date: see below. |
checkOutDate | YYYY-MM-DD | New departure date. If the report has already been filed, Kinapi annuls it and files it again with the new date: see below. |
arrivalTime | hh:mm | New check-in time. |
departureTime | hh:mm | New check-out time. |
numGuests | integer 1-50 | New expected number of guests. |
leadGuest | object | Correct the lead guest details: { name, email, phone }. It is MERGED with what the booking already has, so send only the field that changed and the rest stays as it was; an empty string clears that field. The email matters more than it looks: it is the address the check-in invitation, the reminder and the signed contract go to, so a stale one leaves the guest without their link and nothing errors. |
locale | string | New language for the form, the contract and the emails. Empty returns the booking to the property's own default. |
returnUrl | string | https URL the guest goes back to when they finish the check-in, or when they choose to leave part-way. They go back by TAPPING a button, never on their own: that way they can stay to review what they sent, and opening their link again does not bounce them out of it. Lets you embed the registration inside your own flow. https only and no embedded credentials; an invalid URL returns 400. We check the SHAPE, not that it works: send it complete, including any trailing token or id, because a well-formed but incomplete URL is accepted and fails at the very end, once the guest has already finished. Empty string to remove it. |
maxGuests | integer 1-50 | The property’s real capacity. Guests can adjust the group size from their link, and this is the ceiling they cannot exceed. If the number sent was too high they can always LOWER it, even above the cap. Without it only the global limit applies. Send an empty string (or 0) to remove it. |
contractTemplateId | string | Changes the contract template for this booking. Empty string to fall back to the property’s. If the contract was already signed, the template it was signed with is kept and the response includes a warning: the change only affects later signatures. |
houseRulesId | string | Changes the house rules set for this booking. Empty string to unassign it and hand control back to the property. On an already signed contract nothing changes: the archived PDF keeps the rules it was signed with. |
tipoPago | string | Changes the payment method for this booking. Empty string to unassign it and fall back to the property’s. Useful when the way a stay was collected changes after the booking was created: as long as the report has not been filed, this is the value that will be reported. |
skipContract | boolean | true = this booking carries no terms of your own: the document comes out with the house rules and the legal conditions, nothing else. For stays that do not need a contract. The legal conditions cannot be removed: they carry the notice that data is filed with the Ministry and the data-protection information. |
manualSubmit | boolean | true = this booking’s report does NOT go out in the nightly submission to SES.Hospedajes, nor is it filed again on its own if its dates change. It is reported when you ask for it: from the panel with “Send manually”, or by calling POST /checkins/{reservationId}/ses. For stays you want to review first. The legal deadline for the report still applies. |
source | string | Corrects the booking channel (airbnb, booking, direct…). Informational: it changes nothing with SES. An empty string sets it back to unknown. |
Choosing the contract by length of stay. If your regional rules distinguish regimes by duration — in the Valencian Community, more than 10 days stops being a tourist dwelling and becomes a seasonal tenancy under the LAU — your system makes that call, because it is the one that knows your case and its legal reading:
contractTemplateId = nights > 10 ? SEASONAL_TEMPLATE : TOURIST_TEMPLATE
And if the booking is extended and crosses the threshold, the same PATCH that changes the dates can change the contract. In the panel, each booking shows its nights, its language and which template it has, so you can check that your logic did what you expected.
curl -X PATCH https://api.kinapi.es/api/v1/checkins/R-2026-0412 \
-H "ocp-apim-subscription-key: YOUR_API_KEY" \
-H "content-type: application/json" \
-d '{ "checkOutDate": "2026-08-19" }'Changing the dates of an already filed booking files it again. SES Hospedajes has no modify operation — only create, query and annul — and the report carries the dates. If you move the arrival or departure of a booking whose report has already gone out, Kinapi annuls that communication with the Ministry and files it again with the guests it already had: straight away if the new arrival is today or in the past, and if it is in the future, in that night’s submission, like any other booking.
It annuls first and only then saves. If the annulment does not go through, the booking stays as it was, here and at the Ministry, and the response says so: you never end up with new dates in Kinapi and old ones at SES. Only the dates trigger this; times, guest count or payment method are saved without touching what was filed.
| Field | Type | Description |
|---|---|---|
refiling | 200 | Annulled and saved. anulados: how many communications were annulled. reenviado: true if it was filed again straight away, with the new batch in lote; false if it goes out in the nightly submission for the new arrival. If the immediate refiling failed, error explains why and the nightly submission retries it. retenida: true if the booking is on manual submission (manualSubmit): it was annulled, but the new report does not go out until you call POST /checkins/{reservationId}/ses. |
refiling_blocked · en_vuelo | 409 | The report has been sent and SES has not confirmed it yet, so it cannot be annulled yet. Nothing was saved. Retry in an hour: the SES response is checked hourly. |
refiling_blocked · anulacion_fallida | 503 | SES did not accept the annulment or did not answer. Nothing was saved: the booking keeps its previous dates. Retry later. |
{
…check-in with the new dates…,
"refiling": { "anulados": 2, "reenviado": true, "lote": "…" }
}{
"error": "refiling_blocked",
"code": "en_vuelo",
"details": "El parte está enviado y aún sin confirmar por SES; no se puede anular todavía. Reintenta en una hora."
}curl -X PATCH https://api.kinapi.es/api/v1/checkins/R-2026-0412 \
-H "ocp-apim-subscription-key: YOUR_API_KEY" \
-H "content-type: application/json" \
-d '{ "tipoPago": "TRANS", "houseRulesId": "" }'Guest link validity
The guest link — the url field of the response — is a bearer credential: whoever holds it can register guests on that booking. It expires 14 days after the checkout date; from then on it returns 410 and stops serving data and the contract PDF. If you need to reopen it, create a new check-in.
Within the link, the fields that identify a person — document number, support number and date of birth — are only shown to whoever registered them, and only for a few hours. After that they can be corrected but not read: the rest of the group sees the document masked. This is what stops a forwarded link from being used to look up someone else’s details.
Cancel or delete a check-in
/checkins/{reservationId}Either way the booking leaves the flow: the guest link stops accepting registrations and no report will be filed. What differs is whether a trace remains, and that is not your call: a booking that has not left anything worth keeping is deleted, one that has is cancelled. The response says which in action, so you never have to guess: what was deleted no longer shows up in GET /checkins, what was cancelled still does.
| Field | Type | Description |
|---|---|---|
deleted | action | Nobody had signed, nothing had been filed with the Ministry and the booking had not been billed: it is deleted along with its guests. The response carries reservationId and deletedGuests (how many guests went with it). |
cancelled | action | There was something to keep: the booking moves to the cancelled status and no data is deleted. The response carries the full check-in object, reason with the motive, and ses with the outcome of the annulment. |
| Field | Type | Description |
|---|---|---|
firmado | reason | There is a signed contract, a guest who already signed, or archived signature evidence. It is kept sealed for three years: a document that can vanish at the request of whoever issued it proves nothing. |
comunicado | reason | Its reports had already gone to the Ministry. Deleting here would leave a filing there with nothing to explain it and no way to annul it. |
facturado | reason | The booking generated billable usage, and that is accounting history backing an invoice. |
If that booking’s report had already been filed — the comunicado case, which always ends in a cancellation — Kinapi annuls it with SES Hospedajes in the same call: Article 6.3 of RD 933/2021 requires the cancellation to be reported too, and without it the Ministry would still hold a stay that is not going to happen. The response carries ses.anulados with how many communications were annulled, and ses.anulacionError if the Ministry did not accept the annulment. The booking is cancelled either way and Kinapi retries the annulment every hour, up to 72 attempts (three days); if it does not get through, it emits the ses.cancel_failed webhook. You can also repeat the DELETE to retry it on the spot. When the Ministry accepts the annulment, the ses.cancelled webhook is emitted, with reason: "cancelled". In the deleted case there is, by definition, nothing to annul.
Cancelled while the report was in flight. If the report had been sent and SES had not confirmed it yet, it cannot be annulled yet: for the Ministry it does not exist so far. The booking is cancelled anyway, and as soon as the confirmation arrives Kinapi annuls those communications; the booking stays cancelled.
Accepting an annulment is not processing it. SES processes annulments asynchronously, just like filings: Kinapi checks the annulment batch every hour and, if the Ministry rejected it, emits ses.cancel_failed. The communication still stands and needs checking in SES.HOSPEDAJES. The state of each annulment is in the annulment field of GET /checkins/{reservationId}/ses.
curl -X DELETE https://api.kinapi.es/api/v1/checkins/R-2026-0412 \ -H "ocp-apim-subscription-key: YOUR_API_KEY"
{
"action": "deleted",
"reservationId": "R-2026-0412",
"deletedGuests": 2
}{
…check-in with "status": "cancelled"…,
"action": "cancelled",
"reason": "comunicado",
"ses": { "anulados": 3 }
}