API for integratorsv1
Guest registration with the authorities, and online check-in, for property management systems.
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.
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.
| Capability | State |
|---|---|
| Read properties, reservations, check-in and registration status | Live |
| SES receipt: confirmed status and communication code | Live |
| Read reservations from the host's own channel manager | Live |
| Test mode with no real submission to authorities | Live |
Cursor pagination on /reservas | Live |
| Per-key rate limiting | Live |
Create, update and cancel reservations, with retained external_id | Live |
| Outbound webhooks for check-in and registration events | Live — on request |
| Inbound Channex webhook, for second-level latency | Live |
| Embeddable check-in SDK | Later |
Authentication
LiveOne 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.
Identifiers carry their data model
LiveEvery 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.
Reading data
LiveThe 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 }
| Filter | Effect |
|---|---|
desde, hasta | By arrival date (YYYY-MM-DD) |
propiedad_id | One property only, prefix included |
limite | Rows per page. Default 100, maximum 500 |
cursor | Continue 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.
{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.
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.
estado | Meaning |
|---|---|
pendiente | Nothing has gone out yet. |
enviado | SES has the file and gave us a batch number. Not validated yet. |
confirmado | SES processed it without errors. codigo_comunicacion is the receipt. |
error | SES rejected it, or it could not be sent. error says why. |
anulado | It 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.
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.
Writing reservations
LiveCreating, 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.
{ "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.
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/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.
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.estado | When |
|---|---|
sin_parte | Nothing had been submitted. The usual case: the submission goes out on the evening of the arrival day. |
anulado | A submission was filed and the cancellation came before the arrival date or on the same day: we voided it in SES. |
no_se_anula | The stay had already started. The guests were lodged, so the submission is true and stays filed. We never void it automatically. |
error | SES 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.
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).
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.
Outbound webhooks
On requestSo 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.
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.
When the host already uses a channel manager
LiveYou 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.
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.
Test mode
LiveA 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
Who is liable for the guest report
Not small print. It is the first thing to be clear about.
| Party | Responsible for |
|---|---|
| Host | Compliance before the administration. Set by law (Spanish RD 933/2021), not by contract |
| Integrator (PMS) | The data arriving complete and on time |
| GoToCheck | Transmitting 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.
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.
Current limits
Listed on purpose, so nothing comes as a surprise.
- 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.
- 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.
Errors
| Code | When |
|---|---|
400 | The identifier is missing its cm- / sin- prefix |
401 | Header missing, or the key is invalid or revoked |
403 | The key does not have write access enabled |
404 | The id is well formed but nothing matches it on this account |
405 | Method not allowed — properties are not writable, and PUT does not exist |
429 | Rate 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.