Documentação oficial
Índice da documentação Descarregar OpenAPI

Orbis API: integração de sites

Guia para ligar o site da Brasille, ou de outro restaurante, ao Orbis. API v1 publicada em produção em 6 de novembro de 2026. Documentação revista em 9 de novembro de 2026.

  • Documentação pública: docs.orbisgroup.pt.
  • Contrato OpenAPI 3.1 para Postman e outras ferramentas: openapi.json.
  • Base da API: https://api.orbisgroup.pt/integrations/v1.
  • Os exemplos usam IDs e valores fictícios. Obter sempre os IDs e preços atuais no catálogo da unidade; estes exemplos não são os preços ou produtos reais da Brasille.

Começar pela Brasille

  1. O proprietário ou administrador entra na conta real da Brasille no Orbis e abre Definições > API e site > Nova credencial.
  2. Seleciona a unidade da Brasille, um nome para a integração e apenas as permissões necessárias. A validade pode ser de 30, 90 ou 365 dias.
  3. Guarda o segredo apresentado uma única vez numa variável privada do servidor do site, por exemplo ORBIS_API_KEY.
  4. O servidor do site consulta /catalog para apresentar categorias, produtos, tamanhos, fotos, alergénios e regras de meio a meio.
  5. O servidor envia pedidos de levantamento para /orders e pedidos de reserva para /reservations; guarda os IDs devolvidos para consultar os estados.

O navegador do visitante comunica com o servidor do site. É esse servidor que chama o Orbis. A chave nunca deve entrar em JavaScript público, variáveis NEXT_PUBLIC_/VITE_, URLs, repositórios, capturas, analytics ou logs. Não é necessário configurar CORS para esta ligação entre servidores.

Este guia documenta a integração externa. Não usar os endereços internos /products/save, /orders/save ou a sessão de um utilizador para integrar o site.

Autenticação e permissões

Todas as operações exigem HTTPS e o cabeçalho:

Authorization: Bearer <CHAVE_PRIVADA>
Accept: application/json

Nos pedidos POST, acrescentar Content-Type: application/json. O segredo real começa por orbis_sk_; não confundir com o prefixo resumido apresentado na lista de credenciais. As chaves simuladas da Demo não funcionam na API real.

A empresa e a unidade são determinadas pela chave. Não enviar organization_id, restaurant_id, preços, totais ou customer_id no pedido externo. Os schemas recusam campos adicionais.

Permissão Autoriza
catalog:read Consultar o catálogo da unidade
orders:write Criar pedidos na unidade
orders:read Consultar pedidos originados pela mesma chave
reservations:read Consultar disponibilidade e reservas originadas pela mesma chave
reservations:write Criar pedidos de reserva na unidade

Uma chave expirada ou revogada deixa de funcionar. Remover ou despromover o proprietário/administrador que a criou também pode retirar o acesso. A criação de pedidos e reservas exige uma conta com acesso operacional válido.

Rotação: os pedidos e reservas pertencem à credencial que os criou. Uma nova chave não consegue consultar os registos de uma chave anterior, mesmo sendo da mesma unidade. Planear a substituição antes de expirar, mantendo a chave antiga válida até concluir os acompanhamentos em curso; depois revogá-la. Não há migração automática desses acessos.

Endereços disponíveis

Método Caminho relativo à base Permissão Sucesso
GET /catalog catalog:read 200
POST /orders orders:write 201
GET /orders/{id} orders:read 200
GET /availability?dia=2026-12-01&pessoas=2 reservations:read 200
POST /reservations reservations:write 201
GET /reservations/{id} reservations:read 200

As respostas são objetos JSON diretamente, sem uma camada data. Os nomes em português no catálogo e nas reservas fazem parte do contrato e devem ser preservados. A referência OpenAPI descreve todos os campos e tipos aceites.

Catálogo e imagens

GET /catalog

curl --fail-with-body 'https://api.orbisgroup.pt/integrations/v1/catalog' \
  -H "Authorization: Bearer ${ORBIS_API_KEY}" \
  -H 'Accept: application/json'

Resposta ilustrativa, com uma categoria e um produto:

{
  "restaurant": {
    "id": "11111111-1111-4111-8111-111111111111",
    "name": "Restaurante de exemplo",
    "slug": "restaurante-exemplo",
    "address": null,
    "postal_code": null,
    "city": null,
    "phone": null,
    "timezone": "Europe/Lisbon",
    "opening_hours": null,
    "currency": "EUR"
  },
  "categorias": [
    {
      "categoria": "Pizzas",
      "ordem": 0,
      "itens": [
        {
          "id": "22222222-2222-4222-8222-222222222222",
          "nome": "Pizza A",
          "descricao": "Descrição de exemplo",
          "preco": 9.9,
          "alergenios": ["Glúten", "Leite"],
          "disponivel": true,
          "fotos": [],
          "variantes": [
            {
              "id": "33333333-3333-4333-8333-333333333333",
              "nome": "25 cm",
              "preco": 9.9,
              "disponivel": true,
              "alergenios": ["Glúten", "Leite"],
              "chave_tamanho": "25cm",
              "preco_meio_a_meio": null
            }
          ],
          "meio_a_meio": { "grupo": "pizzas", "regra_preco": "highest" }
        }
      ]
    }
  ]
}

categorias e itens podem estar vazios. O produto ou a variação indisponível não deve ser apresentado como comprável. variantes: [] significa que o produto usa o preço base; quando existirem variações, selecionar uma opção válida e enviar o seu variant_id.

fotos contém as referências guardadas no Orbis, pela ordem de capa e ordenação. Pode estar vazio; não existe um mínimo de fotografias. Não assumir que todas as referências são URLs absolutas: uma referência relativa de armazenamento precisa de uma origem de publicação conhecida. Esta API não fornece URLs assinadas nem concede acesso ao armazenamento privado. Confirmar o formato das referências do catálogo da Brasille antes de ligar a galeria.

A moeda é restaurant.currency. preco é um valor absoluto na unidade monetária principal, por exemplo 9.9 corresponde a 9,90 EUR quando a moeda é EUR. Não é um valor em cêntimos nem um acréscimo ao preço base. O catálogo não fornece uma decomposição fiscal ou um campo de taxa de IVA: não inventar essa informação.

Usar estes preços de integração como referência. Os overrides do menu público antigo não são usados pelo motor que cria estes pedidos. Receitas, quantidades de insumos, stock, custos, margens e dados de outras empresas não são devolvidos.

Tamanhos e pizza meio a meio

Cada tamanho tem o seu id, preço, disponibilidade e alergénios. A ficha técnica por tamanho fica no Orbis; o site envia apenas os identificadores da escolha.

O restaurante define a regra de preço de cada grupo:

regra_preco Cálculo
highest Maior preço dos dois sabores no tamanho escolhido
average Média dos dois preços, arredondada aos cêntimos
fixed preco_meio_a_meio configurado para o tamanho

Só combinar dois produtos distintos que permitam meio a meio, com o mesmo grupo, regra_preco e chave_tamanho. Para fixed, ambos têm de ter o mesmo preço fixo configurado. meio_a_meio: null desativa combinações nesse produto.

Uma pizza meio a meio é uma linha, uma quantidade e exatamente duas metades. O primeiro par em halves tem de coincidir com product_id e variant_id da linha. O backend valida a combinação, calcula o preço e guarda o histórico. O consumo de insumos usa metade da ficha de cada sabor, multiplicada pela quantidade, nas transições operacionais existentes. O site não envia nem calcula baixas de stock.

Criar e acompanhar pedidos

POST /orders

Corpo para um pedido de levantamento com um tamanho:

{
  "request_id": "44444444-4444-4444-8444-444444444444",
  "type": "takeaway",
  "guest_name": "Cliente de exemplo",
  "guest_phone": "910000000",
  "note": "Nota opcional do pedido",
  "items": [
    {
      "product_id": "22222222-2222-4222-8222-222222222222",

      "variant_id": "33333333-3333-4333-8333-333333333333",
      "quantity": 1,
      "note": "Nota opcional do item"
    }
  ]
}

Exemplo de uma linha meio a meio, a colocar em items:

{
  "product_id": "22222222-2222-4222-8222-222222222222",
  "variant_id": "33333333-3333-4333-8333-333333333333",
  "quantity": 1,
  "halves": [
    {
      "product_id": "22222222-2222-4222-8222-222222222222",
      "variant_id": "33333333-3333-4333-8333-333333333333"
    },
    {
      "product_id": "55555555-5555-4555-8555-555555555555",
      "variant_id": "66666666-6666-4666-8666-666666666666"
    }
  ]
}
Campo Regra
request_id UUID novo por intenção de envio; reutilizar apenas em repetição do mesmo envio
type takeaway ou dine_in; delivery não é aceite nesta versão
table_id UUID de uma mesa válida da unidade, obrigatório em dine_in; não existe aqui um endpoint para listar mesas
guest_name, guest_phone, note Opcionais; até 160, 40 e 500 caracteres, respetivamente
items Entre 1 e 200 linhas
items[].quantity Número maior que zero, até 999 e com no máximo duas casas decimais
items[].note Opcional, até 300 caracteres
items[].variant_id Obrigatório para produtos que tenham variações; omitir nos produtos sem opções

Resposta 201 ilustrativa para a linha normal:

{
  "id": "77777777-7777-4777-8777-777777777777",
  "order_number": "1042",
  "status": "placed",
  "type": "takeaway",
  "subtotal": 9.9,
  "total": 9.9,
  "created_at": "2026-12-01T19:00:00Z",
  "items": [
    {
      "id": "88888888-8888-4888-8888-888888888888",
      "product_id": "22222222-2222-4222-8222-222222222222",
      "variant_id": "33333333-3333-4333-8333-333333333333",
      "product_name": "Pizza A",
      "variant_name": "25 cm",
      "composition": null,
      "quantity": 1,
      "unit_price": 9.9,
      "total": 9.9,
      "note": "Nota opcional do item",
      "status": "pending"
    }
  ]
}

Numa linha meio a meio, composition é uma lista de duas partes com product_id, variant_id, product_name, variant_name, size_key, ratio e unit_price. Cada ratio é 0.5; composition[].unit_price é o preço do sabor, não o total da pizza. O valor cobrado pela linha é items[].unit_price e o total da linha é items[].total.

GET /orders/{id}

Consultar o ID devolvido na criação, por exemplo /orders/77777777-7777-4777-8777-777777777777. A resposta 200 tem a mesma estrutura da criação, com os estados atuais. Só é visível um pedido criado por esta chave.

Os estados operacionais podem incluir placed, accepted, in_preparation, ready, served, out_for_delivery, delivered e cancelled; itens podem ter pending, in_preparation, ready, served e cancelled. O contrato devolve strings: tratar um valor novo sem bloquear a página. placed significa pedido registado, não pagamento recebido.

Usar a resposta do servidor para mostrar o preço final e guardar o ID no servidor do site. Não mostrar sucesso só porque o visitante clicou em finalizar. Nesta versão não há webhook externo de estados: o acompanhamento faz-se por consulta, com frequência moderada e respeitando os limites.

Disponibilidade e pedidos de reserva

GET /availability

Enviar dia como data do calendário da unidade (AAAA-MM-DD) e pessoas como inteiro de 1 a 50. Usar a timezone devolvida no catálogo para apresentar o calendário.

curl --fail-with-body \
  'https://api.orbisgroup.pt/integrations/v1/availability?dia=2026-12-01&pessoas=2' \
  -H "Authorization: Bearer ${ORBIS_API_KEY}"

Resposta 200 ilustrativa:

{
  "dia": "2026-12-01",
  "pessoas": 2,
  "horas": [
    { "slot_at": "2026-12-01T20:00:00Z", "lugares_livres": 12, "cabe": true }
  ],
  "policy": {
    "deposit_mode": "none",
    "deposit_amount": 0,
    "payment_deadline_minutes": 30,
    "cancellation_hours": 24,
    "terms": "Condições de exemplo do restaurante."
  }
}

horas pode estar vazio e policy pode ser null. Oferecer apenas os horários com cabe: true. A consulta não bloqueia lugares: a criação volta a verificar capacidade e pode devolver 409 se o horário deixar de estar disponível.

slot_at é um instante absoluto. Enviá-lo como inicio sem reinterpretar o fuso horário. Apresentar o horário em restaurant.timezone; não acrescentar nem retirar uma hora manualmente no horário de verão.

deposit_mode pode ser none, per_booking ou per_person. Apresentar terms e as condições ao visitante antes de recolher a aceitação. policy: null não obriga a inventar termos.

POST /reservations

{
  "request_id": "99999999-9999-4999-8999-999999999999",
  "inicio": "2026-12-01T20:00:00Z",
  "pessoas": 2,
  "nome": "Cliente de exemplo",
  "telefone": "910000000",
  "email": "cliente@example.com",
  "nota": "Nota opcional",
  "aceita_politica": true
}
Campo Regra
request_id UUID, com as mesmas regras de repetição dos pedidos
inicio ISO 8601 com Z ou offset; usar o slot_at escolhido
pessoas Inteiro de 1 a 50; sujeito à configuração e capacidade do restaurante
nome Obrigatório, de 2 a 120 caracteres
telefone Obrigatório, de 6 a 30 caracteres
email Opcional, endereço válido até 160 caracteres
nota Opcional, até 500 caracteres
aceita_politica true quando existe uma política de reserva; enviar após aceitação do visitante

Resposta 201 ilustrativa para uma reserva que aguarda condições:

{
  "id": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
  "starts_at": "2026-12-01T20:00:00Z",
  "party_size": 2,
  "duration_minutes": 90,
  "status": "pending",
  "deposit_due": 10,
  "deposit_status": "pending",
  "payment_expires_at": "2026-12-01T19:30:00Z"
}

Estes valores exemplificam um caso com sinal e não correspondem necessariamente à política do exemplo de disponibilidade anterior. O backend respeita a política real, aprovação, antecedência e capacidade. Só status: confirmed deve aparecer como reserva confirmada. pending é um pedido de reserva pendente. Não apresentar um pagamento como concluído a partir de deposit_due ou de um HTTP 201.

GET /reservations/{id}

Consultar o ID guardado, por exemplo /reservations/aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa. A resposta 200 tem os mesmos campos da criação e os estados atuais. Só é visível uma reserva originada pela mesma chave.

payment_expires_at pode ser null. A API não devolve códigos de gestão privados nem os contactos de outros clientes. A equipa gere confirmação e alterações no módulo Reservas do Orbis. A resposta não inclui uma ligação de pagamento de sinal e este contrato não inicia pagamentos automaticamente.

Repetição segura e limites

Gerar um UUID para request_id no servidor do site ao iniciar cada envio. Guardar esse identificador e o corpo enviado. Se houver timeout ou perda da resposta, repetir com a mesma chave, o mesmo request_id e os mesmos dados. Os POST devolvem 201 também na repetição bem-sucedida.

A mesma chave, operação e request_id não criam dois registos. Alterar o corpo com o mesmo identificador devolve 409. Um novo identificador representa uma nova intenção e pode criar outro pedido ou reserva. Os identificadores não autorizam recuperar registos de outras credenciais.

Os limites atuais são de 60 consultas por minuto por rota e IP e 10 criações por minuto por rota e IP, sujeitos também aos limites gerais do serviço. Um servidor de site pode partilhar o mesmo IP entre visitantes. Respeitar Retry-After quando devolvido e aplicar espera progressiva nos erros temporários; não repetir erros 400/401/403 em ciclo.

As respostas da integração usam Cache-Control: no-store. Não guardar respostas autenticadas em caches partilhadas/CDN. Guardar apenas os dados necessários ao acompanhamento no servidor do site, com controlo de acesso por visitante; conhecer um ID não deve permitir a um visitante consultar a encomenda de outro através do seu site.

Erros e tratamento no site

Erros de acesso ou negócio têm normalmente esta forma:

{
  "error": "integration_request_conflict",
  "message": "integration_request_conflict"
}

Erros de formato devolvem HTTP 400 com error: validation_error e uma lista issues com o campo e a causa. A estrutura dessa lista segue o validador do backend; não depender de uma mensagem traduzida fixa.

HTTP Código habitual Ação recomendada
400 validation_error ou integration_request_invalid Rever campos, opções, disponibilidade do produto e aceitação de política
401 unauthorized Rever segredo completo, validade, revogação e ambiente real/Demo
403 forbidden Rever permissões, unidade, acesso do criador e estado operacional da conta
404 not_found Rever ID e credencial de origem; não revelar dados de outra chave
409 integration_request_conflict Rever reutilização de request_id; numa reserva, voltar a consultar disponibilidade
429 too_many_requests Respeitar Retry-After quando presente e reduzir frequência
500 internal_server_error Não confirmar sucesso; guardar request_id e repetir apenas o mesmo envio quando apropriado

Um 400 pode representar uma escolha que já não é válida. Voltar a consultar o catálogo antes de pedir ao cliente para selecionar outra opção. Não substituir silenciosamente um sabor ou tamanho. Um 409 de reserva pode indicar indisponibilidade, não apenas um envio alterado.

Exemplo de ligação no servidor

Exemplo para Node.js com fetch, sem bibliotecas adicionais. Colocar exclusivamente no servidor do site. Não executar exemplos POST em produção como simples teste: criam pedidos ou reservas reais.

const base = 'https://api.orbisgroup.pt/integrations/v1'
const key = process.env.ORBIS_API_KEY
if (!key) throw new Error('Falta configurar ORBIS_API_KEY no servidor')

async function orbis(path, payload) {
  const response = await fetch(`${base}${path}`, {
    method: payload === undefined ? 'GET' : 'POST',
    headers: {
      Authorization: `Bearer ${key}`,
      Accept: 'application/json',
      ...(payload === undefined ? {} : { 'Content-Type': 'application/json' }),
    },
    ...(payload === undefined ? {} : { body: JSON.stringify(payload) }),
    signal: AbortSignal.timeout(15000),
    cache: 'no-store',
  })
  const body = await response.json()
  if (!response.ok) {
    // Tratar o erro no servidor; não registar a chave nem contactos do cliente.
    throw new Error(`Orbis ${response.status}: ${body.error ?? 'erro'}`)
  }
  return body
}

const catalog = await orbis('/catalog')
// Numa intenção de compra, guardar um crypto.randomUUID() e o corpo antes
// de chamar orbis('/orders', payload). Repetir o mesmo envio após timeout.

Proteger os endereços do próprio site contra abuso, validar as escolhas do visitante e associar os IDs Orbis à sessão desse visitante no servidor. A chave dá acesso ao site como integração; não autentica cada visitante final.

O que esta versão não disponibiliza

  • Pagamento online do pedido, checkout Stripe do restaurante ou ligação para pagar o sinal de uma reserva. O Stripe das licenças Orbis é outro fluxo.
  • Pedidos de entrega (delivery), gestão de moradas ou cálculo de taxas de entrega nesta API externa.
  • Cancelar/editar pedidos e reservas, alterar o catálogo ou listar todas as mesas, todos os pedidos ou todas as reservas da empresa.
  • Webhooks externos de alteração de estado.
  • Receitas, fichas técnicas, quantidades de insumos, custos, margens ou acesso à base de dados.

Para a Brasille, já é possível integrar catálogo com tamanhos e meio a meio, pedidos de levantamento e pedidos de reserva. Se o site precisar de checkout com pagamento, entrega ou outras operações acima, é necessário acrescentar e publicar esse contrato antes de mostrar o fluxo como disponível.

Checklist de entrega da Brasille

  • Criar a credencial na conta real e unidade correta, com permissões mínimas e prazo de rotação definido.
  • Retirar o catálogo fixo do site como fonte de verdade; consumir os dados da unidade no Orbis.
  • Confirmar fotos, tamanhos, disponibilidade e política de meio a meio efetivamente configurados pelo restaurante.
  • Guardar o segredo só no servidor; nunca o expor no browser ou na documentação.
  • Mostrar os totais e estados devolvidos pelo Orbis; conservar IDs e request_id por envio.
  • Apresentar condições de reserva e distinguir pendente de confirmada.
  • Validar manualmente o fluxo com autorização do responsável, sem cobrar ou enviar notificações a clientes por engano.

A implementação de referência está em infra/api/src/modules/integrations/. O contrato OpenAPI é gerado a partir dos schemas publicados do backend. O contrato interno de variações destina-se à gestão autenticada e não substitui esta API de integração.