API for integratorsv1

Guest registration with the authorities, and online check-in, for property management systems.

Draft 24 Sep 2026
01

What this solves

GoToCheck handles guest registration with the authorities and the guest-facing online check-in. A PMS or booking engine selling in Spain needs both, and normally does not build them: it is years of work per country, and administrations change their interfaces without notice.

Spain SES.Hospedajes (SOAP), Mossos d'Esquadra, Ertzaintza
International Portugal (SIBA), Italy (Alloggiati), Croatia (eVisitor), Andorra, Thailand (TM.30)
Check-in Multilingual form, document OCR, signature capture
Smart locks Nuki, TTLock, Tedee, Bold, YACAN

What GoToCheck is not: a PMS. We do not handle rates, availability or payments. If you already have that, we sit beside it.

02

Status, without the gloss

Everything marked Live has been tested against production. Everything marked Proposed is specified but not built, and its shape may still change. Tell us what you need and we will shape it around your integration — that is easier now than later.

CapabilityState
Read properties, reservations, check-in and registration statusLive
SES receipt: confirmed status and communication codeLive
Read reservations from the host's own channel managerLive
Test mode with no real submission to authoritiesLive
Cursor pagination on /reservasLive
Per-key rate limitingLive
Create, update and cancel reservations, with retained external_idLive
Outbound webhooks for check-in and registration eventsLive — on request
Inbound Channex webhook, for second-level latencyLive
Embeddable check-in SDKLater
03

Authentication

Live

One key per integrator. Long, stable, and revocable at any time.

GET /api/v1/propiedades HTTP/1.1
Host: app.gotocheck.pro
Authorization: Bearer gtc_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

No JWT and no expiry: there is no person behind the key signing in, there is a server calling every few minutes with no screen to re-enter a password on. Same approach Channex takes with user-api-key.

A key is bound to one host account and sees only that account's data. Missing header, or a revoked key: 401.

Rate limits

60 calls per minute and 600 per hour, per key. Over that, 429 with a Retry-After header in seconds — and it tells you when a slot actually frees up, not a round number:

{ "error": "Has llegado al límite de 60 llamadas por minuto.
            Vuelve a intentarlo en 34 segundos." }

The limit is per key: one integrator going over does not take service away from another. If your integration needs more, say so and we raise it — it is a number, not a technical ceiling.

04

Identifiers carry their data model

Live

Every id takes the form cm-123 or sin-123, and is used verbatim in the URL:

GET /api/v1/reservas/sin-946612
GET /api/v1/reservas/cm-4471/registro
GET /api/v1/reservas?propiedad_id=sin-156

The prefix is not decoration. Hosts with a channel manager and hosts without one are stored in two separate data models, each numbering independently, so reservation 1 exists in both at once. An id without a prefix is rejected with 400 rather than returning the neighbouring reservation.

The tipo field (cm / sin_cm) remains in every response. The prefix does not replace it, it just makes it usable in a URL.

Or use your own reference

Anywhere a reservation id is accepted, ext- plus your own external_id works too — on reads as well as writes:

GET    /api/v1/reservas/ext-MY-REF-001
GET    /api/v1/reservas/ext-MY-REF-001/registro
PATCH  /api/v1/reservas/ext-MY-REF-001
DELETE /api/v1/reservas/ext-MY-REF-001

That means you never have to store our identifiers at all: create a reservation with your reference and keep using it.

05

Reading data

Live
GET/api/v1/propiedades

The properties on the account. Not paginated: it returns all of them in one response. An account's property count is bounded in a way its reservation count is not, so there is nothing to page through. If you ever have enough for that to hurt, tell us.

{ "propiedades": [
    { "id": "sin-156", "tipo": "sin_cm",
      "nombre": "Acogedor piso Granada centro",
      "direccion": "Camino de Ronda, 191", "ciudad": "Granada",
      "pais": "España", "activa": true }
  ], "total": 1 }
GET/api/v1/reservas
FilterEffect
desde, hastaBy arrival date (YYYY-MM-DD)
propiedad_idOne property only, prefix included
limiteRows per page. Default 100, maximum 500
cursorContinue from where the previous page ended

With no filters it returns the reservations that have not ended yet, which is almost always what you want.

While the response carries next_cursor, there are more pages:

{ "reservas": [ … ], "total": 100,
  "next_cursor": "MjAyNi0wOS0yN3xzaW58OTc5Njc5" }

GET /api/v1/reservas?limite=100
GET /api/v1/reservas?limite=100&cursor=MjAyNi0wOS0yN3xzaW58OTc5Njc5

No next_cursor means you have reached the end. The cursor is opaque: send it back verbatim, never build one by hand — a tampered cursor returns 400.

It is cursor-based rather than offset on purpose: reservations come from two data models merged by date, and with offset a new reservation arriving mid-traversal would shift the rows and either repeat one or lose one.

GET/api/v1/reservas/{id}

{id} is either ours (sin-946612) or yours (ext-MY-REF-001) — see section 4.

{ "id": "sin-946612", "tipo": "sin_cm",
  "referencia": "c0a6a005-b955-43ac-b4f4-1ef9e3db7166",
  "origen": "AirBNB",
  "propiedad": { "id": "sin-156", "nombre": "Acogedor piso Granada centro" },
  "huesped": { "nombre": "Sehee Noh", "email": "…", "idioma": "en" },
  "fecha_entrada": "2026-09-23", "fecha_salida": "2026-09-27",
  "numero_huespedes": 3,
  "enlace_checkin": "https://app.gotocheck.pro/checkin/sin/reserva/…/",
  "enlace_extras": "…",
  "huespedes": [
    { "nombre": "Sehee Noh", "documento": "M932R9713",
      "tipo_documento": "pasaporte", "nacionalidad": "Corea del Sur",
      "registrado": true }
  ] }

enlace_checkin is the key piece: the URL you hand the guest. Send it by email or WhatsApp, or open it inside your own product.

GET/api/v1/reservas/{id}/registro

How the check-in and the authority submission are going. The endpoint you will actually poll.

{ "reserva_id": "cm-1042",
  "checkin": { "completado": true, "huespedes_registrados": 2,
               "huespedes_esperados": 2 },
  "parte_autoridades": {
    "estado": "confirmado",
    "enviado": true,
    "huespedes_declarados": 2,
    "sistema": "ses_hospedajes",
    "codigo_comunicacion": "5c1e9a70-0000-4a1b-9f2e-3b7d8c6a1e42",
    "codigos_comunicacion": ["5c1e9a70-0000-4a1b-9f2e-3b7d8c6a1e42"],
    "confirmado_en": "2026-09-27T21:35:02+02:00",
    "error": null } }

What "sent" means, and when you have a receipt

SES answers twice. When we submit, it hands back a batch number straight away: that only means it received the file. It validates afterwards, and we ask for the verdict every hour. If it went through, SES returns the communication code — the official receipt. If not, the reason it was rejected.

estadoMeaning
pendienteNothing has gone out yet.
enviadoSES has the file and gave us a batch number. Not validated yet.
confirmadoSES processed it without errors. codigo_comunicacion is the receipt.
errorSES rejected it, or it could not be sent. error says why.
anuladoIt was voided in SES and is no longer filed. anulado_en says when.

enviado (the boolean) is kept for compatibility and means "went out", confirmed or not. Rely on estado. If guests were added after the first submission they go in a second batch: codigos_comunicacion lists every receipt, and the reservation only counts as confirmado once all of them are. The host sees the same code in the reservation screen of their panel.

Only SES Hospedajes has a confirmation step. Mossos, Ertzaintza and the international systems go from enviado to error or stay at enviado.

Important

parte_autoridades is per reservation, not per guest. SES receives one submission per reservation with every traveller inside it: a three-person submission returns a single communication code (confirmed in production). On a guest, registrado means "was included in the submission that went out".

sistema can be ses_hospedajes, mossos, ertzaintza, a country code for the international systems, test, or an empty string when the property has no authority configured.

06

Writing reservations

Live

Creating, updating and cancelling reservations is available. Your key needs write access enabled — a key handed over for reading does not start creating reservations just because the method exists.

POST/api/v1/reservas
{ "external_id": "MY-REF-001",
  "propiedad_id": "sin-156",
  "fecha_entrada": "2026-10-01",
  "fecha_salida": "2026-10-05",
  "numero_huespedes": 2,
  "huesped": { "nombre": "Jane Doe", "email": "jane@…",
               "telefono": "+34600111222", "idioma": "en" } }

external_id is your own reference, the one you keep in your system. You use it afterwards to update the reservation, so you never have to store ours.

One optional extra field: origen, free text, to record where the booking came from ("Booking.com", "direct", your channel's name). It shows up as origen when reading the reservation and in the host's panel. Left out, it says api.

It is idempotent

Sending the same external_id twice does not create two reservations — it updates the one that is there. A retry after a network timeout duplicates nothing. You get 201 when it was created and 200 when an existing one was updated, so you can tell the two apart without keeping score.

The data model is chosen by the property: a propiedad_id of cm-… creates it on one side, sin-… on the other. A reservation has to live where its property lives.

What it triggers, and what it does not

Guest messages By default it depends on the dates: a future reservation schedules them, a past one does not — importing history must not wake up guests who already left. Force either way with "programar_mensajes": true/false. The response carries mensajes_programados so you know what happened.
Properties The API does not create them. You write reservations onto properties that already exist. Otherwise an account fills with properties that have no address and no authority credentials, and submissions fail with nobody understanding why.
Guests Not writable. The guest fills them in at check-in, and what they write goes to the police submission — it is nobody else's field.
PATCH/api/v1/reservas/{id}
PATCH /api/v1/reservas/sin-946612
PATCH /api/v1/reservas/ext-MY-REF-001

You can address it by our identifier or yours (ext- plus your external_id).

Only what you send is touched. Sending {"fecha_salida": "…"} does not blank the guest's email: anything absent from the request stays as it was. That is why there is PATCH and no PUT — replacing a whole reservation invites clearing fields by omission.

external_id cannot be changed: it is what identifies the reservation.

DELETE/api/v1/reservas/{id}

Cancels the reservation, and tells you what happened to its SES submission:

{ "cancelada": "cm-1042",
  "parte": { "estado": "anulado",
             "motivo": "Cancelled before arrival (03/10/2026): the stay did not take place.",
             "lotes": ["0a1b2c3d-…"] } }
parte.estadoWhen
sin_parteNothing had been submitted. The usual case: the submission goes out on the evening of the arrival day.
anuladoA submission was filed and the cancellation came before the arrival date or on the same day: we voided it in SES.
no_se_anulaThe stay had already started. The guests were lodged, so the submission is true and stays filed. We never void it automatically.
errorSES did not confirm the voiding. We retry up to five times and send parte.anulado when it goes through.

The same rule applies when the cancellation comes from a channel rather than from you.

POST/api/v1/reservas/{id}/registro/anular

Voids the SES submission now, without cancelling the reservation. For what a rule cannot decide: a no-show, or wrong details you are about to send again corrected. It works at any point, stay started or not — here you are the one who knows whether the stay happened.

Returns what /registro returns, with estado: "anulado". 409 if there is no submission filed in SES; 502 if SES does not void it (nothing changes on our side).

Before sending it again

SES voids the whole batch — every guest in it. And SES rejects a resubmission that is byte-for-byte identical to an earlier one, even a voided one: correct the details first.

07

Outbound webhooks

On request

So you don't have to ask every few minutes whether the guest has checked in. We tell you when it happens.

checkin.completado · parte.enviado · parte.confirmado · parte.error · parte.anulado

parte.confirmado arrives when SES validates the submission, carrying the communication code (see section 5). parte.enviado arrives first, when the file goes out: it does not mean SES has accepted it.

Off by default, on every account

These are configured by hand and only for those who ask for them. With no URL set, not a single notification goes out. Tell us and we set yours up.

How it arrives

POST https://your-system.example.com/gotocheck
Content-Type: application/json
X-GoToCheck-Evento: parte.enviado
X-GoToCheck-Evento-Id: 3f2b8c1e-6a4d-4e0b-9c7a-1d5e8f0a2b3c
X-GoToCheck-Firma: 9f8a7c…

{ "evento_id": "3f2b8c1e-6a4d-4e0b-9c7a-1d5e8f0a2b3c",
  "evento": "parte.enviado",
  "ocurrido_en": "2026-09-24T21:00:13+02:00",
  "reserva": "sin-946612",
  "registro": { "checkin": { … }, "parte_autoridades": { … } } }

registro is exactly what GET /reservas/{id}/registro returns. On purpose: if the API said one thing and the notification another, neither would be any use settling a dispute.

Event ID and duplicates

Every notification carries an evento_id, in the body and in the X-GoToCheck-Evento-Id header. It is unique and stays the same across retries: store it and discard anything you have already processed. A retry is the same request byte for byte — same body, same timestamp, same signature.

Each event is sent once per submission. A submission that is rejected, corrected and sent again is a new one: you will receive parte.error for the first and a fresh parte.enviado (and later parte.confirmado) for the second, each with its own evento_id. The same error repeated does not notify twice.

The signature

X-GoToCheck-Firma is an HMAC-SHA256 of the whole body, using the secret handed to you along with the URL. Verify it before acting on a notification: guessing your URL is then not enough to forge one.

Retries

If your server does not answer with a 2xx, we retry six times with growing gaps: 1 minute, 5, 15, 1 hour, 6 hours, 1 day. After that we stop trying — but we keep the record of what was attempted and what your server answered.

The one that matters is parte.error: if an administration rejects a submission and nobody hears about it, the host pays the fine. That is why this is a queue and not a direct call — your server may well be down at the exact moment a submission goes out.

08

When the host already uses a channel manager

Live

You do not need this API to start. GoToCheck can read reservations straight from the host's own channel manager account, read-only. Today: Channex, Lodgify, Smoobu, Guesty, Hostaway, Hospitable, Cloudbeds, MangoBeds, Little Hotelier and others.

Channex, in detail

Using the host's key, we make three calls, all reads:

GET /api/v1/properties     validate the credential, list properties
GET /api/v1/bookings       filter[arrival_date][gte], filter[property_id]

No POST, no PUT, no DELETE.

Channex does not offer read-only keys — a key carries the same powers as the user who creates it. What you can do is create a separate key limited to the properties you choose (untick "Access to all properties") and revoke it at any time with "Withdraw". That is what we recommend: you never share the credential your own system uses, and we only ever read with it.

On not consuming your events

We do not touch the booking revisions feed and we never send an ack. This matters: the Channex feed is consumed — the acknowledgement marks an event as processed — so two integrations on the same feed interfere with each other. We stay out of it entirely and poll instead. Your own Channex connection cannot tell we are there, and it is impossible for us to consume an event you needed.

Latency today
2 minUpper bound, polling interval
Covers
New · Modified · CancelledNot new bookings only
Cancellations detected by
cancelled, canceled, declined, expired, failed

Getting to seconds Live

You register a webhook in your own Channex account pointing at an endpoint of ours, and reservations arrive in seconds instead of up to two minutes. Ask us for your URL — it carries a token tied to your account.

Three things worth knowing about it:

  • You register it, not us. Creating a webhook is a write (POST /webhooks), and if we did it with your key it would stop being true that we only read. So the URL is yours to paste into your Channex panel.
  • It does not disturb your own integration. Channex webhooks are broadcast, unlike the feed, which is consumed — your PMS keeps receiving its own events exactly as before. We have this running in our own account, where our webhook coexists with eleven belonging to a revenue management tool.
  • It is an accelerator, not a dependency. We never send an ack, and if the endpoint is down nothing is lost: the two-minute poll picks it up anyway.
09

Test mode

Live

A property can be configured with its authority system set to test. The full cycle works — check-in, form, submission generation, status — and nothing leaves for the real administration.

channel manager  →  GoToCheck  →  online check-in  →  submission in test mode
10

Who is liable for the guest report

Not small print. It is the first thing to be clear about.

PartyResponsible for
HostCompliance before the administration. Set by law (Spanish RD 933/2021), not by contract
Integrator (PMS)The data arriving complete and on time
GoToCheckTransmitting correctly what it receives, and raising the alarm when it fails

The system itself confirms this: the authority credentials belong to the host, per property. Submissions go out signed with theirs, never with the integrator's. Technically the integrator is never the declaring party, and that is a line worth not crossing.

Not legal advice

This is a product reading of the regulation. The data-protection side — a PMS processing guest data inside a third party's account is a processing arrangement — should be reviewed by a lawyer.

11

Current limits

Listed on purpose, so nothing comes as a surprise.

  1. A submission is voided as a whole batch. SES has no way to remove one guest from a filed submission: it is voided and sent again corrected.
  2. Test mode is a simulation, not an SES sandbox. See section 9: it validates the path end to end, but SES itself does not check your files. Their pre-production environment can be enabled instead if that is what you need.

What used to be on this list and no longer is: the SES communication code was not stored, so /registro could say a submission went out but not hand you the receipt — it does now, with a confirmado state and its own webhook. Cancelling did not void a filed submission — it does now, before arrival or on the arrival day, and there is an endpoint to void one yourself. GET /reservas capped at 200 rows with no way past it — it paginates by cursor now. There was no rate limiting — there is, per key, with the numbers in section 3. And writing reservations and outbound webhooks were both only specified — both are built.

12

Errors

CodeWhen
400The identifier is missing its cm- / sin- prefix
401Header missing, or the key is invalid or revoked
403The key does not have write access enabled
404The id is well formed but nothing matches it on this account
405Method not allowed — properties are not writable, and PUT does not exist
429Rate limit reached. Retry-After says how long to wait

Draft of 24 September 2026, written alongside the API itself. Sections marked Live are running in production; sections marked Proposed are specified and can still change shape — which means your requirements can shape them. If you integrate this and something is missing, tell us: the gaps listed above came from an integrator's questions.