Documentation
Getting started
The yaReserva API for anyone integrating a system with a hotel: how each request is signed, how to avoid charging twice, and what comes back when something goes wrong.
What this API is
yaReserva is the system a hotel runs and sells with. This API is the door for another program to do part of that work: check what is available and book from the hotel’s own website, charge dinner at the restaurant to the room, or issue the digital key at check-in.
It is not a public catalogue API: every credential belongs to one hotel and to one of its properties, and only sees that property’s data. The hotel creates it from its panel and can switch it off at any time.
| Integration | For whom | Who calls whom |
|---|---|---|
| Own booking engine | A hotel that already has its own search on its website | Your server calls yaReserva |
| Restaurant POS | Soft Restaurant, Parrot, Loyverse or another point of sale | The POS calls yaReserva |
| Door locks | Salto, TTLock, Dormakaba, Assa Abloy and the hotel’s bridge | yaReserva calls your bridge |
Note: Everything on this page applies to all three
What changes is the header prefix and the key prefix, not the mechanics. Read this page once and then go straight to your integration’s section.
Environments
There are two environments and they are two separate worlds. Credentials from one do not work in the other: a test key in production answers 401, and the other way round too. The hotel asks for the test ones in its panel and gets the base URL to point at along with them.
| Environment | Base URL | What happens there |
|---|---|---|
| Test | The base URL comes with your test credentials | Test data. No real charges, no real locks. This is where you build. |
| Production | https://api.yareserva.com | Real guests and real money. The hotel enables it once the integration already works in test. |
Keep the base URL in your configuration, never in code. Going from test to production must be a matter of changing two values: the URL and the credential pair.
Signed authentication
There are no tokens and no sessions. Every request is signed on its own with the connection’s secret, and the secret never travels: what travels is the signature, which is the only thing you can compute if you hold it.
Each integration has its own trio of headers. The name changes, the shape does not: public key, time and signature.
| Integration | Key prefix | Headers |
|---|---|---|
| Booking engine | yreng_ | X-YR-ENG-Key, X-YR-ENG-Timestamp, X-YR-ENG-Signature |
| Restaurant POS | yrpos_ | X-YR-POS-Key, X-YR-POS-Timestamp, X-YR-POS-Signature |
| Door locks | yrlock_ | X-YR-Lock-Key, X-YR-Lock-Timestamp, X-YR-Lock-Signature |
- Serialize the body to JSON and keep those exact bytes. From here on do not reformat it: one extra space changes the signature.
- Take the current time in Unix seconds, in UTC. That number is the timestamp.
- Build the string to sign by joining the timestamp, a dot and the body: “timestamp + "." + body”. On a GET the body is empty, so the string ends at the dot.
- Compute HMAC-SHA256 of that string using the secret as the key, and render it as lowercase hexadecimal.
- Send the three headers: the public key, the timestamp you used, and the signature prefixed with “sha256=”.
X-YR-ENG-Key: yreng_3f9a1c0b7d2e4a6f8b0c1d2e
X-YR-ENG-Timestamp: 1789459391
X-YR-ENG-Signature: sha256=9c1f0a7d4b2e6c8a1f3d5b7e9c0a2d4f6b8e0c2a4d6f8b0c2e4a6d8f0b2c4e6aKEY=yreng_3f9a1c0b7d2e4a6f8b0c1d2e
SECRET=yreng_sk_...
BODY='{"offers":["eyJ2IjoxLCJyIjoi..."],"guest":{"firstName":"Ana","lastName":"Ruiz","email":"ana@correo.mx"},"acceptedTerms":true,"acceptedPrivacy":true}'
TS=$(date +%s)
SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //')
curl -X POST https://api.yareserva.com/api/v1/engine/bookings \
-H "Content-Type: application/json" \
-H "X-YR-ENG-Key: $KEY" \
-H "X-YR-ENG-Timestamp: $TS" \
-H "X-YR-ENG-Signature: sha256=$SIG" \
-H "X-Idempotency-Key: 8c2e1f04-5a6b-4c7d-8e9f-0a1b2c3d4e5f" \
--data-binary "$BODY"const crypto = require('crypto');
function firmar(payload, secret) {
const body = JSON.stringify(payload);
const ts = Math.floor(Date.now() / 1000).toString();
const sig = crypto.createHmac('sha256', secret).update(`${ts}.${body}`).digest('hex');
return { body, ts, sig: `sha256=${sig}` };
}
// Manda `body` tal cual, sin volver a serializar el objeto.import hashlib, hmac, json, time
def firmar(payload, secret):
body = json.dumps(payload, separators=(",", ":")).encode()
ts = str(int(time.time()))
sig = hmac.new(secret.encode(), ts.encode() + b"." + body, hashlib.sha256).hexdigest()
return body, ts, "sha256=" + sigRule: The secret is shown only once
When the connection is created or rotated, the panel shows the secret once and never again: it is stored encrypted and neither the hotel nor we can read it afterwards. Put it in your secret manager right then. If it is lost, the way out is to rotate, and the previous secret stops working immediately.
The timestamp expires
A valid signature stays valid forever if nobody looks at the clock. So we look: the timestamp you send cannot differ from ours by more than five minutes. Outside that window the request is rejected even if the signature is correct.
- Sync the server clock with NTP. The most common 401 is not a badly computed signature, it is a slow clock.
- Generate a fresh timestamp and signature on every retry. Resending the whole request as-is fails as soon as five minutes go by.
- The time goes in seconds, not milliseconds. A value in milliseconds lands thousands of years in the future and is rejected.
Idempotency
A dropped connection does not tell you whether the request arrived. Retrying blind is what duplicates a booking or charges the same dinner twice. That is why everything that creates something carries an idempotency key.
X-Idempotency-Key: 8c2e1f04-5a6b-4c7d-8e9f-0a1b2c3d4e5f- Generate the key once, before the first attempt, and reuse it on every retry of that same operation. A UUID v4 will do.
- The same key returns the same result: the resource that was already created, not a new one and not an error.
- The key is recorded alongside the resource it created. Reusing it with different data creates nothing new and raises no error: it returns the first one. If the data changes, change the key.
- The key accepts letters, digits and . _ : - and fits in 100 characters. A UUID v4 will do.
- The key check and the sale happen inside a lock, so two simultaneous requests with the same key cannot create two resources: they are serialized.
Note: The POS uses the ticket number
For room charges the idempotency key’s job is done by the “externalId” field, the restaurant ticket number. Same idea under another name: retry with the same ticket number and the charge is not duplicated.
Response format
Every response is JSON with the same envelope. When it went well you get “data”; when it did not, you get “error”. Never both. “meta” only shows up on paginated lists.
{
"data": {
"id": "5d0c3f1e-2b7a-4c1d-9e8f-0a1b2c3d4e5f",
"totalCents": 372000,
"currency": "MXN",
"createdAt": "2026-11-20T21:03:11Z"
},
"meta": { "page": 1, "limit": 20, "total": 37 }
}{
"error": {
"code": "VALIDATION_ERROR",
"message": "checkOut must be after checkIn",
"details": { "field": "checkOut" }
}
}| What | How it travels | Example |
|---|---|---|
| Identifiers | UUID v4 as text. Never integers. | 5d0c3f1e-2b7a-4c1d-9e8f-0a1b2c3d4e5f |
| Instants | ISO 8601 in UTC, with a trailing Z. | 2026-11-20T21:03:11Z |
| Days | YYYY-MM-DD, no time. A check-in is a day, not an instant. | 2026-11-20 |
| Amounts | Cents, always an integer and always with its currency. | 372000 MXN is $3,720.00 |
| Errors | The code is stable and in English; the message comes in Spanish, so you can show it as it is. | code: OFFER_EXPIRED |
Careful: Cents do not become decimals along the way
Divide by one hundred only when you print it on screen. If you store the amount as a decimal number, sooner or later a rounding error leaves one peso of difference between your ticket and the guest’s folio.
Errors
The HTTP status says what kind of problem it is and “error.code” says exactly which one. Program against the code, not against the message text: the message is there for a person to read and may change.
Errors
| HTTP | Code | When | What to do |
|---|---|---|---|
| 400 | IDEMPOTENCY_KEY_REQUIRED | The idempotency key is missing on something that creates a resource. | Send it. It is not invented for you. |
| 401 | INVALID_SIGNATURE | Unknown or revoked key, signature that does not match, or time outside the five-minute window. | Check the clock first. Then check that you sign the exact bytes you send. |
| 403 | ORIGIN_NOT_ALLOWED | The request came from a browser on a domain the key has not authorized. | Do not retry. Either call from your server, or have the hotel declare that domain in its panel. |
| 404 | NOT_FOUND | The resource does not exist, or does not belong to this property. | Check the identifier. A credential only sees its own property. |
| 409 | (varios) | The operation clashes with the current state: already cancelled, already paid, no longer accepting charges. Each integration has its own codes, listed in its own section. | Read the resource to see where it stands before deciding. Retrying as-is will clash again. |
| 413 | PAYLOAD_TOO_LARGE | The body is over 64 KB. | Send fewer lines or less detail. |
| 422 | VALIDATION_ERROR | A field is missing or holds an impossible value. | “details.field” says which one. Fix it and send again. |
| 429 | RATE_LIMIT_EXCEEDED | More than 300 requests per minute from the same IP. | Wait for what “Retry-After” says and retry. It is the only 4xx you retry without changing anything. |
| 500 | INTERNAL_ERROR | Something broke on our side. | Retry with the same idempotency key, backing off between attempts. |
| 503 | PAYMENTS_NOT_CONFIGURED | The hotel does not have online payment configured. | Do not retry. Everything else keeps working; the only thing you cannot do is charge. |
Rule: Never store card data
No endpoint in this API takes a card number, a CVV or an expiry date, and none returns them. Card data is captured by Stripe in the guest’s browser and never passes through your system or ours.
Integrating a hotel?
Credentials come out of the hotel’s panel, and sandbox ones are created the same day. If the hotel does not have an account yet, start by registering it.