Saltar para o conteúdo

Para programadores

Liga o teu sistema, ou um assistente de IA, à agenda do salão

Uma API REST, avisos por webhook e um servidor MCP. Quem marca por aqui cumpre as mesmas regras do site do salão: horas livres verificadas, antecedência mínima, sinal e aprovação.

Para donos de salão: para que serve

Não precisas de saber programar. Quem te ajuda a ligar outro sistema, ou um assistente de IA, à agenda usa esta página.

  • Um assistente de IA que marca por ti

    O ChatGPT, o Claude ou outro assistente vê as horas livres e marca, sempre com as regras do teu salão. Dás-lhe uma chave só com as permissões que quiseres.

  • Ligar outro sistema

    A faturação, uma loja online ou um programa feito à medida veem a agenda e criam marcações sem copiar dados à mão.

  • Receber avisos

    Quando uma marcação ou um cliente muda, a Marcela avisa o endereço que escolheres, para o outro sistema estar sempre em dia.

Quem manda és tu: crias e revogas chaves em Definições › API e webhooks, escolhes o que cada chave pode fazer e, se quiseres, limitas uma chave a uma só loja.

Quem a tem

  • No plano Prata acrescenta-se o extra «API, webhooks e MCP» por 12,00 € por mês.

Ver preços

Obter uma chave

As chaves criam-se no painel do salão, por quem tem a permissão (por defeito, o dono).

  1. Entra no painel e abre Definições › API e webhooks.
  2. Escolhe um nome, as permissões da chave e, se quiseres, uma só loja.
  3. Copia a chave quando aparecer: só se vê uma vez. Guarda-a num gestor de palavras-passe. Se a perderes, revoga-a e cria outra.

A chave tem a forma mk_9f2c…. A Marcela só guarda uma impressão digital (SHA-256) dela, por isso ninguém a consegue ler depois.

Permissões de uma chave

  • catalog:readver lojas, serviços e equipa
  • availability:readver horas livres
  • bookings:readler marcações
  • bookings:writemarcar e cancelar
  • customers:readler clientes
  • customers:writecriar clientes

Uma chave de uma só loja não pode ter permissões de clientes, porque veria a lista de clientes do salão inteiro.

Usa a chave só em servidores. Não a ponhas em páginas web nem em apps: a API não responde a pedidos de outros sítios do navegador.

Primeiros pedidos

Todos os pedidos levam o cabeçalho Authorization: Bearer <key>. Primeiro, as horas livres de um serviço (os ids vêm de GET /api/v1/shops e GET /api/v1/catalog):

Endereço base: https://app-teste.hellomarcela.com/api/v1

Horas livres
curl "https://app-teste.hellomarcela.com/api/v1/availability?shop_id=<shop>&service_id=<service>&from=2026-11-02" \
  -H "Authorization: Bearer $KEY"

Depois, marcar. Com Idempotency-Key repetir o pedido não marca duas vezes.

Marcar
curl -X POST "https://app-teste.hellomarcela.com/api/v1/bookings" \
  -H "Authorization: Bearer $KEY" \
  -H "Idempotency-Key: order-123" \
  -H "Content-Type: application/json" \
  -d '{
    "shop_id": "<shop>",
    "start": "2026-11-02T10:30:00Z",
    "services": [{ "service_id": "<service>", "staff_id": "<person>" }],
    "customer": { "name": "Marta", "phone": "912345678" }
  }'

Se o salão pede sinal, a marcação fica em pending_deposit e a resposta traz o endereço de pagamento para dar ao cliente. Com aprovação manual fica em pending_approval.

Regras dos pedidos

Autenticação
Authorization: Bearer <key> em todos os pedidos. Sem chave, ou com uma chave revogada ou expirada, a resposta é 401.
Datas e horas
Instantes em ISO 8601, em UTC nas respostas; nos pedidos aceita-se Z ou um desvio (+01:00). As datas simples (AAAA-MM-DD) são dias no fuso da loja.
Dinheiro
Em cêntimos, em números inteiros.
Listas
50 resultados por página (no máximo 200, com limit). A resposta traz next_cursor: usa-o em cursor para pedir a página seguinte.
Repetir um pedido
No POST de marcações envia Idempotency-Key (até 200 carateres): o mesmo pedido dá a mesma marcação durante 24 horas, em vez de marcar duas vezes. A mesma chave com outro pedido dá 409. Depois de um erro a chave fica livre.
Limite de pedidos
Por minuto e por chave, conforme o plano. Os cabeçalhos X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset dizem quanto sobra; passado o limite, a resposta é 429 com Retry-After.

Erros

Sempre no mesmo formato. O que conta é o code, que não muda; a mensagem é para pessoas.

Exemplo de erro
{
  "error": {
    "code": "slot_taken",
    "message": "That time has just been taken.",
    "details": {}
  }
}

Códigos de erro

  • invalid_request400Pedido inválido; details.field diz qual o campo.
  • unauthenticated401Chave em falta, errada, revogada ou expirada.
  • account_closed403A conta do salão está fechada.
  • not_in_plan403O plano do salão não inclui a API.
  • forbidden_scope403A chave não tem a permissão necessária.
  • forbidden_shop403A chave está limitada a outra loja.
  • not_found404Não existe, ou não é deste salão.
  • method_not_allowed405Método não permitido neste endereço.
  • conflict409O estado atual não permite a operação.
  • idempotency_conflict409A mesma Idempotency-Key foi usada com outro pedido.
  • idempotency_in_progress409Um pedido igual ainda está a ser tratado.
  • slot_unavailable409A hora não está disponível.
  • slot_taken409A hora acabou de ser ocupada.
  • customer_exists409Já existe um cliente com esse telemóvel (details.customer_id).
  • customer_blocked409O salão não aceita marcações deste cliente.
  • booking_limit409O cliente atingiu o limite de marcações.
  • booking_closed409O salão não aceita marcações online agora.
  • coupon_refused422O cupão não pode ser usado.
  • out_of_area422Fora da zona de visitas ao domicílio.
  • rate_limited429Limite de pedidos por minuto atingido; vê Retry-After.
  • internal500Erro do nosso lado; tenta de novo.
  • unavailable503Serviço temporariamente indisponível; tenta de novo.

Referência

Gerada a partir da descrição OpenAPI que a própria API serve, por isso nunca fica desatualizada. Os textos desta parte estão em inglês. Abrir a descrição OpenAPI (JSON)

Catálogo

GET/api/v1/shops

The shops the key can use

Permissão: catalog:read

Resposta:200 object

Erros possíveis:401403429

GET/api/v1/catalog

Services that can be booked online in a shop, and the people who do them

Permissão: catalog:read

shop_id is required unless the key is tied to one shop.

Parâmetros

  • shop_idna pesquisastring (uuid) · opcionalThe shop.

Resposta:200 Catalog

Erros possíveis:400401403404429

Horas livres

GET/api/v1/availability

Free start times for one service (or several, comma separated)

Permissão: availability:read

For several services with their own staff, variant or add-ons use POST.

Parâmetros

  • shop_idna pesquisastring (uuid) · opcionalThe shop (required unless the key is tied to one).
  • service_idna pesquisastring · opcionalOne or more service ids, comma separated (booked one after the other).
  • fromna pesquisastring · opcionalFirst day, YYYY-MM-DD in the shop's time zone.
  • tona pesquisastring · opcionalLast day (inclusive); default: the same day.
  • staff_idna pesquisastring (uuid) · opcionalA person, for the first service.
  • variant_idna pesquisastring (uuid) · opcionalA variant, for the first service.
  • addon_idna pesquisastring · opcionalAdd-ons of the first service, comma separated.
  • limitna pesquisainteger · opcionalMost slots to return.

Resposta:200 Availability

Erros possíveis:400401403404429

POST/api/v1/availability

Free start times for a list of services

Permissão: availability:read

Corpo do pedido AvailabilityRequest

  • shop_idstring (uuid) · opcional
  • fromstring (date) · obrigatório
  • tostring (date) · opcional
  • servicesServiceRequest[] · obrigatório
  • services[].service_idstring (uuid) · obrigatório
  • services[].staff_idstring (uuid) · opcionalA person; omit for whoever is free.
  • services[].variant_idstring (uuid) · opcionalRequired when the service has variants.
  • services[].addon_idsstring (uuid)[] · opcional
  • limitinteger · opcional

Resposta:200 Availability

Erros possíveis:400401403404429

Marcações

GET/api/v1/bookings

Bookings, oldest start first

Permissão: bookings:read

Parâmetros

  • shop_idna pesquisastring (uuid) · opcionalOnly this shop.
  • fromna pesquisastring · opcionalStarts at or after (date-time with offset).
  • tona pesquisastring · opcionalStarts before (date-time with offset).
  • statusna pesquisastring · opcionalComma separated: pending_deposit, pending_approval, confirmed, completed, cancelled, no_show.
  • customer_idna pesquisastring (uuid) · opcionalOnly this customer.
  • limitna pesquisainteger · opcionalPage size.
  • cursorna pesquisastring · opcionalFrom the previous page.

Resposta:200 object

Erros possíveis:400401403429

POST/api/v1/bookings

Make a booking

Permissão: bookings:write

The salon's rules for customers apply (lead time, blocked customers, deposit, approval). If a deposit is due the booking is pending_deposit and the answer carries deposit.pay_url. Send an Idempotency-Key header to make a retry safe.

Parâmetros

  • Idempotency-Keyno cabeçalhostring · opcional

Corpo do pedido BookingRequest

Give customer_id or customer (not both).

  • shop_idstring (uuid) · opcional
  • startstring (date-time) · obrigatório
  • servicesServiceRequest[] · obrigatório
  • services[].service_idstring (uuid) · obrigatório
  • services[].staff_idstring (uuid) · opcionalA person; omit for whoever is free.
  • services[].variant_idstring (uuid) · opcionalRequired when the service has variants.
  • services[].addon_idsstring (uuid)[] · opcional
  • customer_idstring (uuid) · opcional
  • customerCustomerRequest · opcional
  • customer.namestring · opcional
  • customer.phonestring · opcionalE.164, or national (the platform's default country code is added).
  • customer.emailstring (email) · opcional
  • notesstring · opcional
  • coupon_codestring · opcional

Resposta:201 Booking

Erros possíveis:400401403404409422429

GET/api/v1/bookings/{id}

One booking

Permissão: bookings:read

Parâmetros

  • idno endereçostring (uuid) · obrigatório

Resposta:200 Booking

Erros possíveis:401403404429

POST/api/v1/bookings/{id}/cancel

Cancel a booking that is still open

Permissão: bookings:write

Parâmetros

  • idno endereçostring (uuid) · obrigatório

Corpo do pedido (opcional)

  • reasonstring · opcional

Resposta:200 Booking

Erros possíveis:401403404409429

Clientes

GET/api/v1/customers

Customers, oldest first

Permissão: customers:read

(not for keys tied to one shop).

Parâmetros

  • phonena pesquisastring · opcionalExact phone number; numbers without a country code get the platform's default.
  • emailna pesquisastring · opcionalExact email.
  • qna pesquisastring · opcionalPart of the name.
  • limitna pesquisainteger · opcionalPage size.
  • cursorna pesquisastring · opcionalFrom the previous page.

Resposta:200 object

Erros possíveis:400401403429

POST/api/v1/customers

Create a customer

Permissão: customers:write

Answers 409 customer_exists (with details.customer_id) when the phone number is already a customer.

Corpo do pedido CustomerRequest

A name and/or a phone number is required.

  • namestring · opcional
  • phonestring · opcionalE.164, or national (the platform's default country code is added).
  • emailstring (email) · opcional

Resposta:201 Customer

Erros possíveis:400401403409429

GET/api/v1/customers/{id}

One customer

Permissão: customers:read

Parâmetros

  • idno endereçostring (uuid) · obrigatório

Resposta:200 Customer

Erros possíveis:401403404429

Webhooks

Em vez de perguntares sempre se algo mudou, a Marcela avisa o teu servidor. Em Definições › API e webhooks regista um endereço https e escolhe os avisos que queres.

Avisos disponíveis

  • booking.createdMarcação criada: Uma marcação nova, por qualquer canal (também as que esperam sinal ou aprovação).
  • booking.confirmedMarcação confirmada: Uma marcação que esperava sinal ou aprovação passou a confirmada.
  • booking.rescheduledMarcação mudada de hora: A hora (ou o profissional) de uma marcação mudou.
  • booking.cancelledMarcação cancelada: Uma marcação foi cancelada pelo cliente, pela equipa ou por falta de sinal.
  • booking.completedMarcação concluída: A equipa marcou a marcação como feita.
  • booking.no_showCliente faltou: A equipa marcou que o cliente faltou.
  • customer.createdCliente novo: Um cliente novo na ficha do salão (por marcação, conversa, importação ou à mão).

O que recebes

O corpo é um JSON com o aviso e, em data, o mesmo JSON que a API envia nas respostas (a marcação ou o cliente, lidos no momento do envio).

Um aviso
POST /your/endpoint HTTP/1.1
Content-Type: application/json
X-Webhook-Event: booking.created
X-Webhook-Id: 7f0c1d52-…
X-Webhook-Signature: t=1792000000,v1=5257a869…

{
  "id": "7f0c1d52-…",
  "type": "booking.created",
  "created_at": "2026-11-02T09:00:00.000Z",
  "account_id": "…",
  "data": { "id": "…", "status": "confirmed", "starts_at": "…", "customer": { … } }
}

Cabeçalhos: X-Webhook-Event (o tipo), X-Webhook-Id (igual em todas as tentativas: usa-o para ignorar repetições) e X-Webhook-Signature.

Verificar a assinatura

O segredo (começa por whsec_) aparece uma vez, quando crias o endereço. A assinatura é o HMAC-SHA256 de «t.corpo», com t em segundos. Calcula-a sobre o corpo tal como chegou e recusa avisos antigos.

Node.js
import { createHmac, timingSafeEqual } from "node:crypto";

// rawBody: the body exactly as received (a string), not parsed and re-written
export function verify(rawBody, header, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const age = Math.abs(Date.now() / 1000 - Number(parts.t));
  if (!(age <= toleranceSeconds)) return false;
  const expected = createHmac("sha256", secret).update(parts.t + "." + rawBody).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(parts.v1 ?? "");
  return a.length === b.length && timingSafeEqual(a, b);
}
Python
import hmac, hashlib, time

def verify(raw_body: bytes, header: str, secret: str, tolerance_seconds: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    if abs(time.time() - int(parts["t"])) > tolerance_seconds:
        return False
    signed = parts["t"].encode() + b"." + raw_body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))

Entrega e repetições

Responde com 2xx em menos de 10 segundos. Qualquer outra resposta, ou demora, repete o aviso: espera 30 segundos, a dobrar até 3600, no máximo 8 tentativas. Depois de 10 falhas seguidas o endereço desliga-se sozinho e o painel diz porquê.

Não há garantia de ordem: usa o updated_at da marcação para saber qual é a mais recente.

Só chamamos endereços https públicos: endereços privados são recusados e não seguimos redirecionamentos.

Em «Enviar teste», o painel manda um aviso ping para veres a ligação a funcionar.

Assistentes de IA (MCP)

O servidor MCP deixa o ChatGPT, o Claude e outros assistentes ver horas livres e marcar, com a mesma chave e as mesmas regras da API.

Endereço: https://app-teste.hellomarcela.com/api/mcp

Nos clientes que aceitam servidores MCP remotos, a configuração é parecida com esta (o cabeçalho leva a tua chave):

Configuração
{
  "mcpServers": {
    "salon": {
      "url": "https://app-teste.hellomarcela.com/api/mcp",
      "headers": { "Authorization": "Bearer <your key>" }
    }
  }
}

Ferramentas

  • list_shopscatalog:readThe shops (places) of the salon this key can use, with their ids and time zones.
  • list_servicescatalog:readThe services that can be booked online in a shop (id, name, duration, price in cents, variants, add-ons) and the people who do them. Call this first to get the ids.
  • check_availabilityavailability:readFree start times for one or more services between two dates (YYYY-MM-DD, in the shop's time zone). Returns slots with starts_at (UTC) and the person for each service. Always check before booking.
  • create_bookingbookings:writeMake a booking at a start time taken from check_availability. Give customer_id (an existing customer) or customer (name and/or phone; an existing customer with that phone is reused). If the salon asks for a deposit, the booking waits for it and the answer has deposit.pay_url to give the customer. Use the same idempotency_key when retrying.
  • list_bookingsbookings:readBookings, oldest start first. Filter by dates (ISO 8601 with offset), status or customer.
  • get_bookingbookings:readOne booking by id.
  • cancel_bookingbookings:writeCancel a booking that is still open.
  • find_customerscustomers:readCustomers of the salon by exact phone, exact email or part of the name.
  • create_customercustomers:writeCreate a customer (a name and/or a phone number is required). Fails with customer_exists if the phone number is already a customer.

A chave só vê as ferramentas das suas permissões. Um erro dentro de uma ferramenta volta como resultado, para o assistente se corrigir.

Para experimentar sem assistente, pede a lista de ferramentas:

Listar as ferramentas
curl -X POST "https://app-teste.hellomarcela.com/api/mcp" \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/list" }'

Precisas de ajuda?

Fala connosco e dizemos-te como ligar o teu sistema. A resposta chega por email.