GET/api/v1/shops
The shops the key can use
Permissão: catalog:read
Resposta:200 object
Erros possíveis:401403429
Para programadores
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.
Não precisas de saber programar. Quem te ajuda a ligar outro sistema, ou um assistente de IA, à agenda usa esta página.
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.
A faturação, uma loja online ou um programa feito à medida veem a agenda e criam marcações sem copiar dados à mão.
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.
As chaves criam-se no painel do salão, por quem tem a permissão (por defeito, o dono).
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.
catalog:readver lojas, serviços e equipaavailability:readver horas livresbookings:readler marcaçõesbookings:writemarcar e cancelarcustomers:readler clientescustomers:writecriar clientesUma 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.
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
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.
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.
Authorization: Bearer <key> em todos os pedidos. Sem chave, ou com uma chave revogada ou expirada, a resposta é 401.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.X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset dizem quanto sobra; passado o limite, a resposta é 429 com Retry-After.Sempre no mesmo formato. O que conta é o code, que não muda; a mensagem é para pessoas.
{
"error": {
"code": "slot_taken",
"message": "That time has just been taken.",
"details": {}
}
}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.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)
GET/api/v1/shops
Permissão: catalog:read
Resposta:200 object
Erros possíveis:401403429
GET/api/v1/catalog
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
GET/api/v1/availability
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
Permissão: availability:read
Corpo do pedido AvailabilityRequest
shop_idstring (uuid) · opcionalfromstring (date) · obrigatóriotostring (date) · opcionalservicesServiceRequest[] · obrigatórioservices[].service_idstring (uuid) · obrigatórioservices[].staff_idstring (uuid) · opcionalA person; omit for whoever is free.services[].variant_idstring (uuid) · opcionalRequired when the service has variants.services[].addon_idsstring (uuid)[] · opcionallimitinteger · opcionalResposta:200 Availability
Erros possíveis:400401403404429
GET/api/v1/bookings
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
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 · opcionalCorpo do pedido BookingRequest
Give customer_id or customer (not both).
shop_idstring (uuid) · opcionalstartstring (date-time) · obrigatórioservicesServiceRequest[] · obrigatórioservices[].service_idstring (uuid) · obrigatórioservices[].staff_idstring (uuid) · opcionalA person; omit for whoever is free.services[].variant_idstring (uuid) · opcionalRequired when the service has variants.services[].addon_idsstring (uuid)[] · opcionalcustomer_idstring (uuid) · opcionalcustomerCustomerRequest · opcionalcustomer.namestring · opcionalcustomer.phonestring · opcionalE.164, or national (the platform's default country code is added).customer.emailstring (email) · opcionalnotesstring · opcionalcoupon_codestring · opcionalResposta:201 Booking
Erros possíveis:400401403404409422429
GET/api/v1/bookings/{id}
Permissão: bookings:read
Parâmetros
idno endereçostring (uuid) · obrigatórioResposta:200 Booking
Erros possíveis:401403404429
POST/api/v1/bookings/{id}/cancel
Permissão: bookings:write
Parâmetros
idno endereçostring (uuid) · obrigatórioCorpo do pedido (opcional)
reasonstring · opcionalResposta:200 Booking
Erros possíveis:401403404409429
GET/api/v1/customers
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
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 · opcionalphonestring · opcionalE.164, or national (the platform's default country code is added).emailstring (email) · opcionalResposta:201 Customer
Erros possíveis:400401403409429
GET/api/v1/customers/{id}
Permissão: customers:read
Parâmetros
idno endereçostring (uuid) · obrigatórioResposta:200 Customer
Erros possíveis:401403404429
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.
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 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).
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.
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.
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);
}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", ""))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.
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):
{
"mcpServers": {
"salon": {
"url": "https://app-teste.hellomarcela.com/api/mcp",
"headers": { "Authorization": "Bearer <your key>" }
}
}
}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:
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" }'Fala connosco e dizemos-te como ligar o teu sistema. A resposta chega por email.