Quem tem um site próprio, ou um desenvolvedor de confiança, costuma querer uma coisa: mostrar no site as datas livres e o preço certo, e deixar o hóspede pedir a reserva ali, sem abrir outro calendário para conferir. É o canal direto, como mais um canal, ao lado do Airbnb e do Booking.
A API de parceiro do ReservasHub foi feita para isso. Este artigo explica o que é gratuito e o que é add-on, como funciona a autenticação, como criar reservas sem duplicar, e como receber e verificar webhooks com HMAC, com exemplos curtos em Node e Python. Se você ainda se pergunta quando a API é necessária e quando o iCal basta, comece pelo artigo sobre channel manager e iCal.
Um aviso importante: o pagamento fica fora da API
A fronteira é deliberada: a API mostra preço, mas não move dinheiro. Ela não cobra o hóspede, não gera link de pagamento e não faz reembolso. Se o seu site cria uma reserva pela API, ela nasce pendente (segurando as datas) ou confirmada por pedido explícito, e o pagamento acontece por fora, no fluxo que você escolher. O pagamento direto do ReservasHub (Pix e cartão, via add-on) é um produto separado, usado pela página da unidade.
O que é grátis e o que é add-on
| Recurso | Escopo | Custo |
|---|---|---|
| Unidades | read:units | Grátis |
| Disponibilidade (datas ocupadas) | read:availability | Grátis |
| Cotação (valor calculado pelo servidor) | read:quote | Grátis |
| Ler reservas | read:reservations | Add-on |
| Criar e cancelar reservas | write:reservations | Add-on |
| Webhooks de saída | (inclusos no add-on de escrita) | Add-on |
O add-on de escrita e webhooks custa R$ 39 por mês, por conta, e não por unidade. A leitura de disponibilidade, cotação e unidades é grátis em qualquer plano, e é a base de uma landing própria: bloquear datas ocupadas e mostrar o valor.
Todos os planos têm as mesmas funcionalidades; o que varia por plano é só um teto técnico de requisições (300, 600, 1.200 e 3.000 a cada 5 minutos, do Solo ao Gestora), que não é cobrança por chamada.
Autenticação: OAuth2 client credentials
A API é de servidor para servidor. Você troca client_id e client_secret por um token de acesso curto (5 minutos):
// Node 18+ (fetch nativo). API_BASE é o endereço mostrado no portal da API.
const res = await fetch(`${API_BASE}/v1/partner/oauth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'client_credentials',
client_id: process.env.RH_CLIENT_ID,
client_secret: process.env.RH_CLIENT_SECRET,
}),
})
const { access_token } = await res.json()
Pontos que evitam dor de cabeça:
- Não existe refresh token. Quando o token expira, peça outro com as mesmas credenciais.
- O corpo é sempre JSON.
- Allowlist de IP obrigatória. Você informa, por ambiente, os IPs fixos de onde as chamadas saem, e chamada de IP não cadastrado é recusada, mesmo com token válido. Isso significa que o ideal é chamar a API do seu servidor, nunca direto do navegador do hóspede, o que também protege o segredo.
- Guarde o segredo no servidor. Nunca em código de front-end.
Sandbox: teste antes de valer
Há um ambiente de sandbox (/v1/partner/sandbox/*) com credenciais próprias. As respostas seguem o mesmo contrato da produção, com dados de exemplo, e os webhooks de teste são enviados de verdade para a sua URL de sandbox, com dados já mascarados. Escrita e webhooks no sandbox também exigem o add-on ativo, como na produção.
O fluxo típico de um site de reservas
- Consultar a disponibilidade da unidade para as datas.
- Pedir a cotação ao servidor. O valor nunca é calculado nem informado pelo seu site.
- Criar a reserva com
Idempotency-Key. - Cobrar do hóspede por fora da API, e confirmar a reserva quando fizer sentido para o seu fluxo.
// Cotação (a conta é do servidor)
const q = new URLSearchParams({ unit_id, check_in: '2026-12-20', check_out: '2026-12-24' })
const quote = await fetch(`${API_BASE}/v1/partner/quote?${q}`, {
headers: { Authorization: `Bearer ${access_token}` },
}).then((r) => r.json())
// quote.data.total, quote.data.available, quote.data.min_stay_nights ...
Idempotência: não duplicar reserva por causa de um retry
Se a rede cai entre o seu servidor e a API, você não sabe se a reserva foi criada. Reenviar sem cuidado criaria duas. Por isso o POST /v1/partner/reservations exige o cabeçalho Idempotency-Key:
import { randomUUID } from 'node:crypto'
const key = randomUUID() // gere uma por tentativa lógica e reutilize nos retries
const res = await fetch(`${API_BASE}/v1/partner/reservations`, {
method: 'POST',
headers: {
Authorization: `Bearer ${access_token}`,
'Content-Type': 'application/json',
'Idempotency-Key': key,
},
body: JSON.stringify({
unit_id,
check_in: '2026-12-20',
check_out: '2026-12-24',
guest: { name: 'Maria Silva', email: 'maria@exemplo.com', phone: '+5511999999999' },
num_guests: 2,
}),
})
Como a chave se comporta:
- Repetir a mesma chave com o mesmo corpo devolve a mesma resposta, sem criar outra reserva.
- A mesma chave com um corpo diferente é erro seu, e a API recusa.
- Só resposta bem-sucedida é “lembrada”: se a primeira tentativa falhou (por exemplo, datas indisponíveis), um retry executa de novo.
- Datas ocupadas respondem com
409 DATES_UNAVAILABLE.
O corpo da criação recusa campos desconhecidos, e isso é de propósito: não há campo de valor, porque o valor é do servidor.
Webhooks: avisos de saída com assinatura
Em vez de consultar a API a cada minuto, você cadastra uma URL https por ambiente e recebe eventos: reservation.created, reservation.updated, reservation.cancelled e availability.changed. Cada requisição traz:
{ "id": "evt_...", "type": "reservation.created", "created_at": "...", "data": { } }
e cabeçalhos de assinatura: X-RH-Timestamp e X-RH-Signature, no formato v1=<hex>.
A assinatura é um HMAC-SHA256 de "<timestamp>.<corpo cru>" com o segredo do webhook. Verifique sempre, antes de confiar no conteúdo.
Verificação em Node (Express)
import crypto from 'node:crypto'
import express from 'express'
const app = express()
const SECRET = process.env.RH_WEBHOOK_SECRET
// O corpo CRU é indispensável: não use express.json() nesta rota.
app.post('/webhooks/reservashub', express.raw({ type: 'application/json' }), (req, res) => {
const ts = req.header('x-rh-timestamp')
const sig = req.header('x-rh-signature') || ''
const body = req.body.toString('utf8')
if (!ts || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.sendStatus(400) // replay
const expected = crypto.createHmac('sha256', SECRET).update(`${ts}.${body}`).digest('hex')
const ok = sig.split(',').some((p) => {
const got = p.trim().replace(/^v1=/, '')
return got.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(got, 'hex'), Buffer.from(expected, 'hex'))
})
if (!ok) return res.sendStatus(401)
const event = JSON.parse(body)
// trate pelo event.id: a mesma entrega pode chegar mais de uma vez
res.sendStatus(200)
})
Verificação em Python (Flask)
import hmac, hashlib, time, os
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["RH_WEBHOOK_SECRET"].encode()
@app.post("/webhooks/reservashub")
def webhook():
ts = request.headers.get("X-RH-Timestamp", "")
sig = request.headers.get("X-RH-Signature", "")
body = request.get_data() # corpo cru, antes de qualquer parse
if not ts.isdigit() or abs(time.time() - int(ts)) > 300:
abort(400) # replay
expected = hmac.new(SECRET, f"{ts}.".encode() + body, hashlib.sha256).hexdigest()
candidates = [p.strip().removeprefix("v1=") for p in sig.split(",")]
if not any(hmac.compare_digest(c, expected) for c in candidates):
abort(401)
event = request.get_json()
# idempotência: guarde event["id"] e ignore repetições
return "", 200
Três detalhes que costumam derrubar a verificação:
- Corpo cru. Se você interpreta o JSON e o serializa de novo, o HMAC muda.
- Mais de uma assinatura. Durante a troca de segredo, o cabeçalho traz duas entradas
v1=..., e basta uma bater. - Janela de 5 minutos. O carimbo de tempo protege contra reenvio de um corpo antigo.
Entrega: o que esperar
- Pelo menos uma vez. O mesmo evento pode chegar repetido, com o mesmo
id. Desduplique. - Sem garantia de ordem. Use
created_ate, para o estado atual, leia a reserva na API. - Retentativas com espera crescente, até 12 tentativas, ao longo de cerca de 12 horas.
- A assinatura é refeita a cada tentativa, com o carimbo do momento.
- Se o seu endpoint ficar fora do ar por muito tempo, a entrega é pausada e você é avisado.
- Reservas que entram por iCal (Airbnb, Booking) ainda não geram evento. Para essas, use a leitura da API como reconciliação periódica.
Checklist para ir para produção
- IP fixo de saída cadastrado nos dois ambientes.
- Segredos fora do repositório.
Idempotency-Keyem toda criação de reserva.- Webhook verificado com corpo cru, janela de replay e desduplicação por
id. - Reconciliação periódica por leitura de reservas.
- Fluxo de pagamento definido fora da API.
Perguntas frequentes
A API cobra o hóspede? Não. O pagamento fica fora da API.
Preciso do add-on para mostrar disponibilidade e preço no site? Não. Disponibilidade, cotação e unidades são grátis em qualquer plano.
Posso chamar a API direto do navegador? Não é recomendado: a allowlist de IP e o segredo pedem que a chamada saia de um servidor seu.
O sandbox mexe nos meus dados reais? Não. Ele devolve dados de exemplo e não escreve nada.
A API substitui o iCal com Airbnb e Booking? Não. O iCal continua sendo o canal de sincronização com as plataformas; a API serve ao seu canal próprio.
O ReservasHub convive com as plataformas e adiciona o seu canal próprio: calendário unificado, financeiro por canal e, se você precisar, uma API para o seu site.
→ Teste 3 meses grátis, sem cartão, em reservashub.com.br