Integration
Restaurant POS
For the point of sale that closes a tab “to the room”: the charge lands on the stay’s folio and the guest pays it on check-out.
What this solves
The POS keeps taking every other payment as usual: cash, card, transfer. Only one thing changes. When the waiter closes a tab “to the room”, the POS sends us the charge, it lands on the guest’s stay folio, and it is settled at check-out.
It works for Soft Restaurant, Parrot, Loyverse or any other point of sale that can make a signed HTTP call when closing a tab.
- There are three calls: find the guest in a room, post the charge, and void it if the waiter got it wrong.
- Amounts travel in cents and in the booking’s currency.
- The ticket number is the idempotency key. Retrying with the same ticket number does not charge twice.
Credentials
The hotel creates the connection in its panel, under Settings → Restaurant POS, and receives the credential pair once and only once.
| Value | Example | Use |
|---|---|---|
| keyId | yrpos_3f9a1c0b7d2e4a6f8b0c1d2e | Public. Identifies the connection. |
| secret | yrpos_sk_9c1f… (64 hex) | Secret. Signs the requests and never travels inside them. |
Each connection belongs to one property: it only sees and charges stays in that property. The hotel can cap the maximum amount per charge, switch charging off, rotate the secret — the previous one stops working immediately — or revoke the connection outright.
Rule: The secret is shown only once
The panel shows it when it is created and when it is rotated, and never again. Store it right then wherever you keep your secrets; if it is lost, it has to be rotated.
https://api.yareserva.com/api/v1/webhooks/posHow to sign
X-YR-POS-Key: <keyId>
X-YR-POS-Timestamp: <segundos Unix, UTC>
X-YR-POS-Signature: sha256=<hex(HMAC-SHA256(secret, timestamp + "." + cuerpo))>- The body is the exact bytes you send. Sign after serializing the JSON and do not reformat it.
- On GET requests the body is empty: you sign “timestamp + "."”, so the string ends at the dot.
- The POS clock can differ from ours by at most five minutes. Outside that window the request is rejected; sync the clock with NTP.
- Any failure — unknown or revoked key, wrong signature, time out of range — answers 401 INVALID_SIGNATURE and nothing is processed.
- The body cannot exceed 64 KB, and there is a cap of 300 requests per minute per IP.
KEY=yrpos_3f9a1c0b7d2e4a6f8b0c1d2e
SECRET=yrpos_sk_...
BODY='{"externalId":"TK-000123","roomNumber":"204","amountCents":45000,"description":"Cena mesa 7"}'
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/webhooks/pos/charges \
-H "Content-Type: application/json" \
-H "X-YR-POS-Key: $KEY" \
-H "X-YR-POS-Timestamp: $TS" \
-H "X-YR-POS-Signature: sha256=$SIG" \
--data-binary "$BODY"const crypto = require('crypto');
const body = JSON.stringify(payload);
const ts = Math.floor(Date.now() / 1000).toString();
const sig = crypto.createHmac('sha256', secret).update(`${ts}.${body}`).digest('hex');
// headers: X-YR-POS-Key, X-YR-POS-Timestamp: ts, X-YR-POS-Signature: `sha256=${sig}`import hashlib, hmac, json, time
body = json.dumps(payload, separators=(",", ":")).encode()
ts = str(int(time.time()))
sig = hmac.new(secret.encode(), ts.encode() + b"." + body, hashlib.sha256).hexdigest()Find the guest
/rooms/{roomNumber}/guestsWho is staying in a room, so the waiter can confirm with the guest before posting the charge.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
| roomNumber | string | Yes | The room number as the waiter types it. |
Response
{
"data": {
"items": [
{
"stayId": "5d0c3f1e-2b7a-4c1d-9e8f-0a1b2c3d4e5f",
"roomNumber": "204",
"guest": "J. Pérez",
"reservationCode": "•••••••2345",
"checkOut": "2026-09-17"
}
]
}
}Errors
| HTTP | Code | When | What to do |
|---|---|---|---|
| 404 | ROOM_NOT_IN_HOUSE | Nobody is staying in that room. | Show it on the waiter’s screen and have them take another form of payment. |
Note: It returns the minimum, on purpose
Only the first initial and the first surname, and the reservation code down to its last four characters. Enough for the waiter to confirm out loud without the restaurant screen showing a guest’s details to whoever walks past.
Charge to the room
/chargesPosts the consumption to the stay’s folio. On the folio it reads “POS <connection>: <description>”.
Request body
| Name | Type | Required | Description |
|---|---|---|---|
| externalId | string | Yes | 1–100 characters: letters, digits and . _ : -. It is the ticket number and must be unique per connection. |
| roomNumber | string | Yes | Room number. It can be omitted if you send stayId or reservationCode. |
| stayId | UUID | No | The one from the search. Use it when there is more than one stay in the room. |
| reservationCode | string | No | Full reservation code, if the guest gives it. |
| amountCents | integer | Yes | Greater than zero and within the connection’s cap, if the hotel set one. |
| currency | string | No | If present, it must match the booking’s currency. |
| description | string | Yes | 1–120 characters. This is what the guest reads on their folio. |
| lines | array | No | If present, the sum of qty × unitCents must equal amountCents exactly. Discounts go in with a negative unitCents and the tip as one more line. |
Request
{
"externalId": "TK-000123",
"roomNumber": "204",
"stayId": "5d0c3f1e-2b7a-4c1d-9e8f-0a1b2c3d4e5f",
"amountCents": 45000,
"currency": "MXN",
"description": "Cena mesa 7",
"lines": [
{ "name": "Filete", "qty": 2, "unitCents": 21000 },
{ "name": "Propina", "qty": 1, "unitCents": 3000 }
]
}Response
{
"data": {
"chargeId": "b7e4c1d0-3a2f-4e5b-8c7d-9e0f1a2b3c4d",
"externalId": "TK-000123",
"status": "posted",
"duplicate": false,
"amountCents": 45000,
"currency": "MXN",
"roomNumber": "204",
"guest": "J. Pérez",
"reservationCode": "•••••••2345",
"receivedAt": "2026-09-14T21:03:11Z",
"voidedAt": null
}
}Errors
| HTTP | Code | When | What to do |
|---|---|---|---|
| 401 | INVALID_SIGNATURE | Invalid signature, time or key. | Check the clock before anything else. |
| 404 | ROOM_NOT_IN_HOUSE | Nobody in house matching those details. | Confirm the room with the guest and search again. |
| 409 | CHARGES_NOT_ALLOWED | The stay no longer accepts charges — checked out or not yet arrived — or the hotel switched charging off for this connection. | Do not retry. Have the restaurant take another form of payment. |
| 409 | MULTIPLE_STAYS_IN_ROOM | There are two stays in the same room. | Retry sending stayId, the one from the search. |
| 409 | EXTERNAL_ID_REUSED | That ticket number was already used with a different amount or room. | Use a new ticket number. The same number must mean the same charge. |
| 413 | PAYLOAD_TOO_LARGE | Body over 64 KB. | Send fewer detail lines. |
| 422 | VALIDATION_ERROR | A field is missing or the lines do not add up to the total. | “details.field” says which one. |
Void a charge
/charges/{externalId}/voidCreates a credit note on the folio. The original charge is not deleted: it stays, with its note beside it.
Request body
| Name | Type | Required | Description |
|---|---|---|---|
| reason | string | Yes | Why it is being voided. Visible on the folio and in the hotel’s panel. |
Request
{ "reason": "Se cargó a la mesa equivocada" }Response
{
"data": {
"chargeId": "b7e4c1d0-3a2f-4e5b-8c7d-9e0f1a2b3c4d",
"externalId": "TK-000123",
"status": "voided",
"amountCents": 45000,
"currency": "MXN",
"voidedAt": "2026-09-14T21:41:02Z"
}
}Errors
| HTTP | Code | When | What to do |
|---|---|---|---|
| 404 | CHARGE_NOT_FOUND | There is no charge with that ticket number on this connection. | Check the ticket number: you only see charges from your own connection. |
| 409 | CHARGE_ALREADY_VOIDED | It was already voided, by the POS or by the front desk. | Treat it as success: the charge is no longer on the folio. |
| 409 | CHARGES_NOT_ALLOWED | The guest’s account is already closed. | The front desk makes the adjustment from the panel. |
| 422 | VALIDATION_ERROR | “reason” is missing. | Send the reason. |
Idempotency and retries
- Retry with the same externalId on any network error, timeout or 5xx. If the charge had already landed, you get 200 with “duplicate”: true and the same chargeId: it is not charged twice.
- The charge record and the folio movement are written in the same transaction. Two simultaneous retries cannot duplicate the charge.
- Generate a fresh timestamp and signature on every retry: the old signature expires after five minutes.
- Do not retry a 4xx without fixing the request. The exception is 429, which you retry respecting Retry-After.
- Voiding is safe to repeat: the second time it answers 409 CHARGE_ALREADY_VOIDED, which for you means the same as success.
Careful: Ticket numbers are not reused
If your POS restarts numbering each shift or each day, combine the ticket number with the date or the till before sending it. Two different consumptions sharing an externalId make the second look like a retry of the first, and it never reaches the guest’s folio.
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.