← Voltar ao blog
Automação

API para o seu site de reservas diretas: o que é grátis, o que é add-on, OAuth2, idempotência e webhooks com HMAC

Como usar a API de parceiro do ReservasHub no seu site de reservas diretas: leitura grátis, escrita e webhooks por R$ 39/mês, OAuth2, sandbox, idempotência e verificação HMAC em Node e Python. O pagamento fica fora da API.

2 de outubro de 2026 · 8 min de leitura · 1600 palavras
📲

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

RecursoEscopoCusto
Unidadesread:unitsGrátis
Disponibilidade (datas ocupadas)read:availabilityGrátis
Cotação (valor calculado pelo servidor)read:quoteGrátis
Ler reservasread:reservationsAdd-on
Criar e cancelar reservaswrite:reservationsAdd-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

  1. Consultar a disponibilidade da unidade para as datas.
  2. Pedir a cotação ao servidor. O valor nunca é calculado nem informado pelo seu site.
  3. Criar a reserva com Idempotency-Key.
  4. 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:

  1. Corpo cru. Se você interpreta o JSON e o serializa de novo, o HMAC muda.
  2. Mais de uma assinatura. Durante a troca de segredo, o cabeçalho traz duas entradas v1=..., e basta uma bater.
  3. 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_at e, 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-Key em 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

Leia também

Centralize suas reservas hoje

Teste 3 meses grátis, sem cartão de crédito e sem fidelidade.

Teste 3 meses grátis, sem cartão