Documentação · API v1

Documentação da API

Ligue o Urdume ao seu ERP, PLM ou loja online: crie produtos, envie documentos, emita passaportes e receba avisos por webhook.

Introdução

A API do Urdume é uma API REST sobre HTTPS. Os pedidos levam JSON no corpo (Content-Type: application/json) e as respostas são sempre JSON. O envio de documentos usa multipart/form-data.

Endereço base

https://urdume.site/api/v1
  • Todos os objetos têm "id" (identificador com 26 caracteres) e "object" (o tipo: product, batch, document, passport…).
  • As datas são números inteiros em Unix time (segundos, UTC).
  • Para alterar um objeto usa-se POST no endereço do objeto, só com os campos a mudar.
  • Cada resposta traz o cabeçalho Request-Id: indique-o se precisar de ajuda do suporte.

Autenticação

Cada pedido leva uma chave secreta no cabeçalho Authorization. As chaves criam-se no painel, em Programadores, e o valor completo só é mostrado uma vez.

curl https://urdume.site/api/v1/products \
  -H "Authorization: Bearer sk_test_…"
  • Chaves sk_live_ trabalham com os dados reais; chaves sk_test_ com dados de teste.
  • A chave identifica a empresa: não é preciso (nem possível) indicar a empresa no pedido.
  • Guarde as chaves no servidor (variáveis de ambiente). Nunca as coloque em código que corre no browser ou numa aplicação móvel.
  • Se uma chave for exposta, revogue-a no painel e crie outra: a revogação é imediata.

Modo de teste

Com uma chave sk_test_ tudo funciona como em produção, mas sobre dados separados: os produtos, documentos e passaportes de teste nunca se misturam com os reais e não contam para os limites do plano.

  • Todos os objetos têm "livemode": true nos dados reais e false nos de teste.
  • A página pública de um passaporte de teste diz claramente que é um passaporte de teste, e não ocupa o endereço GS1 do produto.
  • As chaves de teste funcionam em qualquer plano. As chaves de produção exigem um plano com API (Pro ou Enterprise).
  • No painel, o interruptor "Dados de teste" mostra os dados criados em modo de teste.

Erros

Os erros usam os códigos HTTP habituais e um corpo JSON com o tipo, um código estável, uma mensagem para pessoas e, quando se aplica, o parâmetro em causa.

{
  "error": {
    "type": "invalid_request_error",
    "code": "invalid_value",
    "message": "Indique uma percentagem entre 0 e 100.",
    "param": "data.recycled_content"
  }
}
HTTPtypeQuando
400 invalid_request_error Parâmetro em falta ou inválido ("param" diz qual).
401 authentication_error Chave em falta, inválida ou revogada.
403 permission_error O plano não inclui a API em produção, conta só de leitura ou suspensa, limite do plano atingido.
404 invalid_request_error O objeto não existe nesta empresa ou neste modo (produção/teste).
409 idempotency_error / invalid_request_error Idempotency-Key reutilizada com outro pedido; passaporte que já existe.
429 rate_limit_error Demasiados pedidos: aguarde o tempo indicado em Retry-After.
500 api_error Erro do nosso lado. A mensagem traz uma referência para o suporte.

Programe a sua integração com base em "code" (estável) e não no texto de "message", que pode mudar.

Listas e paginação

As listas devolvem os objetos mais recentes primeiro, dentro de um objeto "list". Use "limit" (1 a 100, por omissão 10) e, para a página seguinte, "starting_after" com o id do último objeto recebido.

curl "https://urdume.site/api/v1/products?limit=50&starting_after=01J8Z…" \
  -H "Authorization: Bearer sk_test_…"

{
  "object": "list",
  "data": [ { "id": "01J8Y…", "object": "product", … } ],
  "has_more": true,
  "url": "/api/v1/products"
}

Idempotência

Para repetir um POST em segurança (por exemplo depois de uma falha de rede), envie o cabeçalho Idempotency-Key com um valor único criado por si, como um UUID. Durante 24 horas, repetir o mesmo pedido com a mesma chave devolve a resposta original, sem criar nada em duplicado.

curl https://urdume.site/api/v1/passports \
  -H "Authorization: Bearer sk_live_…" \
  -H "Idempotency-Key: 6f1c1a52-8a0e-4c0e-9a57-2f6d1a3b9c10" \
  -H "Content-Type: application/json" \
  -d '{"product": "01J8Z…"}'
  • As respostas repetidas trazem o cabeçalho Idempotent-Replayed: true.
  • Usar a mesma chave com um pedido diferente dá 409 (idempotency_key_reused).
  • Se o primeiro pedido ainda estiver a ser processado, a repetição recebe 409 (idempotency_key_in_use): tente de novo um pouco depois.

Limite de pedidos

Cada chave pode fazer até 100 pedidos por minuto. Acima disso a API responde 429 com o cabeçalho Retry-After (segundos a aguardar). Em importações grandes, espace os pedidos e respeite esse valor.

Produtos

Um produto é um modelo de artigo (por exemplo, "T-shirt básica, ref. TS-100"). Os dados do passaporte ficam em "data", com uma chave por campo do modelo de campos (ver Modelos de campos). Os valores escritos pela API ficam marcados no painel com a origem "API".

PedidoO que faz
GET /products Lista os produtos. Filtro: status=active|archived.
POST /products Cria um produto.
GET /products/{id} Lê um produto.
POST /products/{id} Altera um produto (só os campos enviados).
ParâmetroDescrição
name Nome do produto (obrigatório ao criar, até 160 caracteres).
reference A sua referência interna (obrigatória ao criar, até 80 caracteres, única).
gtin Opcional. Código de barras GTIN/EAN (8 a 14 dígitos, com dígito de controlo válido).
template Opcional, só ao criar. Código do modelo de campos (por omissão "textile").
status Só ao alterar: "archived" arquiva, "active" reativa.
data Objeto {"chave": valor}. Só as chaves enviadas são alteradas; null apaga o valor.
curl https://urdume.site/api/v1/products \
  -H "Authorization: Bearer sk_test_…" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "T-shirt básica",
    "reference": "TS-100",
    "gtin": "4006381333931",
    "data": {
      "brand": "A Minha Marca",
      "country_making": "PT",
      "fibre_composition": [
        {"fibre": "cotton", "percent": 95},
        {"fibre": "elastane", "percent": 5}
      ]
    }
  }'

A resposta inclui "completeness": quantos campos obrigatórios estão preenchidos e quais faltam ("missing") para poder emitir o passaporte.

Lotes

Um lote é uma produção concreta de um produto. Pode ter valores próprios nos campos que o modelo define "por lote" (por exemplo a fábrica ou a data de fabrico), que se sobrepõem aos do produto no passaporte do lote.

PedidoO que faz
GET /products/{id}/batches Lista os lotes do produto.
POST /products/{id}/batches Cria um lote: batch_number (obrigatório, até 20 letras, números, ".", "_" ou "-"), production_date (AAAA-MM-DD), quantity, data.
POST /products/{id}/batches/{lote} Altera os valores próprios do lote ("data").

Documentos

Envie fichas técnicas, faturas, certificados e relatórios de ensaio (PDF, JPEG, PNG ou WebP, até 20 MB). Os ficheiros ficam cifrados. Pode pedir logo a extração dos dados com IA.

PedidoO que faz
GET /documents Lista os documentos não arquivados.
POST /documents Envia um documento (multipart/form-data): file (obrigatório), kind, product, extract.
GET /documents/{id} Lê um documento e o estado da última extração.
POST /documents/{id}/extract Pede (ou repete) a extração com IA.
curl https://urdume.site/api/v1/documents \
  -H "Authorization: Bearer sk_test_…" \
  -F "file=@ficha-tecnica.pdf" \
  -F "kind=tech_sheet" \
  -F "product=01J8Z…" \
  -F "extract=true"
  • kind: tech_sheet (ficha técnica), invoice (fatura), certificate (certificado), test_report (relatório de ensaio), declaration (declaração) ou other.
  • A extração corre em segundo plano: o documento passa por "status": "queued" e "processing" e termina em "review" (há sugestões para validar) ou "failed". Nessa altura recebe o webhook document.extracted (ou document.extraction_failed); em alternativa, consulte GET /documents/{id}.
  • As sugestões da IA aparecem em "extraction.suggestions", com a confiança e a citação do documento.
  • A API nunca aplica sugestões sozinha: uma pessoa com o papel Validador confirma os valores no painel, e só então passam para o produto.

Passaportes

Um passaporte é emitido para um produto (modelo) ou para um lote, quando todos os campos obrigatórios estão preenchidos. Cada versão publicada é imutável e tem o SHA-256 do seu conteúdo.

PedidoO que faz
GET /passports Lista os passaportes (sem o conteúdo).
POST /passports Emite um passaporte: product (obrigatório) e, para um lote, batch.
GET /passports/{id} Lê um passaporte com o conteúdo da versão atual.
POST /passports/{id}/versions Publica uma nova versão com os dados atuais: change_note (obrigatório, até 500 caracteres).
POST /passports/{id}/withdraw Retira o passaporte: reason (obrigatório, até 300 caracteres). Não é reversível.
CampoDescrição
url Endereço público permanente do passaporte (o destino do QR Code e do NFC).
uid_url Endereço pelo identificador único do passaporte.
gs1_key Caminho GS1 Digital Link (/01/{gtin}[/10/{lote}]) quando o produto tem GTIN; null em modo de teste.
nfc.ndef_hex Mensagem NDEF (registo URI) em hexadecimal, para gravar em etiquetas NFC.
nfc.ntag_tlv_hex A mesma mensagem dentro do bloco TLV usado nas etiquetas NTAG.
items_count Número de artigos (peças com número de série) deste passaporte.
content_sha256 SHA-256 do conteúdo da versão atual.
content O conteúdo do passaporte (só em GET /passports/{id} e nas respostas de escrita).

Se faltarem campos obrigatórios, a resposta é 400 com o código passport_missing e a lista dos campos em falta na mensagem.

Artigos (números de série)

Um artigo é uma peça concreta, com o seu número de série, o seu endereço e o seu QR Code, por baixo do passaporte de um modelo ou de um lote. Mostra o conteúdo desse passaporte e o histórico da própria peça. Os artigos não têm versões: quando o passaporte recebe uma versão nova, todas as peças a mostram.

PedidoO que faz
GET /passports/{id}/items Lista os artigos do passaporte. Filtros: serial (número de série exato), run (série criada num pedido).
POST /passports/{id}/items Cria artigos (até 5000 por pedido), com as suas séries ou geradas pelo Urdume.
GET /items/{id} Lê um artigo.
POST /items/{id}/void Anula um artigo (peça destruída, série criada por engano): reason (obrigatório). Não é reversível.
ParâmetroDescrição
serials Lista das suas séries. Cada uma com até 20 letras, números, ".", "_" ou "-". Se enviar este parâmetro, os seguintes são ignorados.
quantity Quantas séries gerar (1 a 5000).
mode "sequential" (por omissão: 000001, 000002…) ou "random" (10 caracteres aleatórios, mais difíceis de adivinhar).
prefix Opcional. Até 10 caracteres antes do número, por exemplo "TS24-".
start Opcional, só em "sequential". Número inicial; por omissão continua a seguir ao último usado com esse prefixo.
curl https://urdume.site/api/v1/passports/01J8Z…/items \
  -H "Authorization: Bearer sk_test_…" \
  -H "Idempotency-Key: producao-2027-03-lote-7" \
  -H "Content-Type: application/json" \
  -d '{"quantity": 500, "prefix": "TS24-"}'

{
  "object": "item_run",
  "id": "01J9A…",
  "passport": "01J8Z…",
  "created_count": 500,
  "first_serial": "TS24-000001",
  "last_serial": "TS24-000500",
  "items_url": "/api/v1/passports/01J8Z…/items?run=01J9A…"
}
  • A série é única por produto, sem distinguir maiúsculas de minúsculas. Se alguma já existir, nada é criado (409, serial_taken).
  • Cada artigo tem "url": o endereço a gravar no QR Code e no NFC dessa peça. Com GTIN, segue o GS1 Digital Link (/01/{gtin}/21/{série}).
  • Os artigos criados por mês contam para o limite do plano. Em modo de teste não contam, mas há um teto de 1000 por mês.
  • Use sempre Idempotency-Key ao criar séries geradas: repetir o pedido sem ela cria outra série.

Eventos pós-venda

Reparações, revendas e reciclagem, registadas numa peça (artigo) ou num passaporte de modelo ou lote. Os eventos publicados aparecem na página pública. O conteúdo de um evento não pode ser alterado nem apagado: um erro corrige-se anulando o evento.

PedidoO que faz
GET /lifecycle_events Lista os eventos. Filtros: item, passport, status=pending|published|rejected|voided.
POST /lifecycle_events Regista um evento (fica publicado).
GET /lifecycle_events/{id} Lê um evento.
POST /lifecycle_events/{id}/void Anula um evento publicado: reason (obrigatório).
POST /lifecycle_events/{id}/files Anexa uma foto ou comprovativo (multipart/form-data, campo file; PDF, JPEG, PNG ou WebP até 10 MB; até 5 por evento).
GET /lifecycle_events/{id}/files Lista os anexos do evento (também vêm em "files" no objeto do evento).
GET /lifecycle_events/{id}/files/{file}/content Descarrega o ficheiro original (fica no registo de atividade).
ParâmetroDescrição
item O artigo (peça) a que o evento diz respeito. Em alternativa a "passport".
passport O passaporte de modelo ou lote, quando não há artigo.
type "repair" (reparação), "resale" (revenda) ou "recycling" (reciclagem).
occurred_on Data do evento (AAAA-MM-DD), não futura.
country Opcional. Código do país com duas letras (PT, ES…).
actor_name Opcional. Quem fez (oficina, loja, reciclador). Aparece em público.
description Opcional, até 1000 caracteres. Aparece em público: não inclua dados pessoais de clientes.
  • Os eventos registados por parceiros com link (reparadores, recicladores…) e por consumidores na página pública (se a empresa ligar essa opção em Definições › Empresa) chegam com "status": "pending" e só ficam públicos depois de aprovados por uma pessoa, no painel. Recebe o webhook lifecycle_event.recorded quando chegam.
  • "source" diz quem registou: "brand" (painel), "api", "third_party" (parceiro com link) ou "consumer" (quem tem a peça, na página pública; fica sempre "pending").
  • Os anexos nunca aparecem em público: a página e o JSON públicos dizem só se o evento tem comprovativo ("evidence"). Um anexo apagado no painel fica com "removed": true e o conteúdo deixa de existir.

Sessões alojadas (como o Stripe Checkout)

A forma mais simples de ligar a sua loja: o seu servidor cria uma sessão para uma referência, indicando quem vai preencher ("operator"), e abre o endereço "url" num separador. É uma página própria do Urdume, como o Stripe Checkout: sem conta Urdume e sem o painel, só com os passos para completar os dados, emitir o passaporte e levar o QR Code. No fim, a pessoa carrega em "Concluir e voltar" e regressa ao "success_url". O seu site não precisa de guardar nada: o passaporte fica sempre em "public_url".

Criar a sessão e abrir "url" (redirecionamento ou novo separador)

curl https://urdume.site/api/v1/passport_sessions \
  -H "Authorization: Bearer sk_test_…" \
  -H "Content-Type: application/json" \
  -d '{
    "reference": "TS-100",
    "name": "T-shirt básica",
    "operator": {"name": "Ana Silva", "email": "ana@loja.exemplo.pt"},
    "success_url": "https://loja.exemplo.pt/admin/produto/100?dpp={SESSION_ID}",
    "cancel_url": "https://loja.exemplo.pt/admin/produto/100",
    "serials": ["TS100-01", "TS100-02"]
  }'

{
  "id": "01J9C…",
  "object": "passport_session",
  "status": "open",
  "url": "https://urdume.site/sessao/01J9C…/x7Kq…",
  "operator": {"name": "Ana Silva", "email": "ana@loja.exemplo.pt"},
  "reference": "TS-100",
  "public_url": "https://urdume.site/r/a-sua-marca/TS-100",
  "passport": null,
  "expires": 1790086400
}
ParâmetroDescrição
reference A sua referência do produto (obrigatória). Se ainda não existir no Urdume, o produto é criado.
name Nome do produto: obrigatório só quando a referência é nova.
operator Obrigatório: quem vai preencher a página, já autenticado no seu sistema. operator[name] (até 160 caracteres) e, opcional, operator[email]. Fica no registo de atividade e nas versões que emitir.
data Opcional, só para produtos novos: valores iniciais dos campos (como em POST /products).
success_url Para onde a pessoa volta ao concluir (obrigatório; HTTPS). "{SESSION_ID}" é trocado pelo ID da sessão.
cancel_url Opcional. Para onde volta se sair sem concluir.
serials Opcional. Números de série das peças (até 5000). Ao concluir, com o passaporte emitido, são criados os artigos que faltam.
metadata Opcional. Até 20 pares chave/valor de texto, devolvidos na sessão e no webhook.
expires_at Opcional. Quando a sessão expira (data Unix): entre 30 minutos e 24 horas a partir de agora. Por omissão, 24 horas.
  • O "url" é um segredo: abra-o só para a pessoa indicada em "operator" (redirecionamento ou novo separador; nunca o mostre em páginas públicas nem o envie por email). Fica preso ao primeiro browser que o abre e deixa de funcionar quando a sessão termina.
  • A sessão conclui-se uma vez, quando o passaporte fica emitido (ou atualizado). GET /passport_sessions/{id} devolve o estado (open, completed ou expired) e "outcome": issued (passaporte novo), version (nova versão) ou unchanged (os dados já estavam publicados).
  • Ao concluir recebe o webhook passport_session.completed, com "passport", "passport_url", "outcome" e "items_created". Use-o para atualizar o seu sistema mesmo que a pessoa feche o separador sem voltar.
  • Emitir exige uma pessoa identificada: a página pede a quem preenche que confirme os dados em nome da sua empresa, e o registo de atividade guarda o nome indicado em "operator".

Endereço pela referência

Cada empresa tem um identificador público (ver Programadores). O passaporte de um produto fica sempre em /r/{identificador}/{referência} e o de uma peça em /r/{identificador}/{referência}/{série}. O Urdume reencaminha para a página pública do passaporte. Use estes endereços no seu site, nos QR Codes e no NFC: não mudam e não obrigam a guardar IDs do Urdume.

O seu identificador: GET /account

curl https://urdume.site/api/v1/account -H "Authorization: Bearer sk_test_…"

{
  "object": "account",
  "name": "A Sua Marca, Lda.",
  "handle": "a-sua-marca",
  "reference_url": "https://urdume.site/r/a-sua-marca/"
}

Exemplos

https://urdume.site/r/a-sua-marca/TS-100
https://urdume.site/r/a-sua-marca/TS-100/TS100-01
https://urdume.site/r/a-sua-marca/TS-100?modo=teste
  • A referência pode ter letras, números, ".", "_", "~" e "-" (até 80). Referências com outros caracteres não têm endereço curto.
  • Enquanto o passaporte não for emitido, o endereço responde 404.
  • "?modo=teste" procura nos dados de teste (criados com chaves sk_test_).

Portal incorporável

Mostre o estado de um produto dentro do seu ERP ou PLM, sem a sua equipa mudar de aplicação: os dados que faltam para o passaporte, os documentos (com envio de ficheiros) e os passaportes emitidos. O seu servidor cria uma sessão curta para um produto; a sua página embute o componente do Urdume com o client_secret dessa sessão.

1. No seu servidor: criar a sessão

curl https://urdume.site/api/v1/portal_sessions \
  -H "Authorization: Bearer sk_test_…" \
  -H "Content-Type: application/json" \
  -d '{"product": "01J8Z…", "origin": "https://erp.a-sua-empresa.pt"}'

{
  "id": "01J9B…",
  "object": "portal_session",
  "product": "01J8Z…",
  "origin": "https://erp.a-sua-empresa.pt",
  "allow_upload": true,
  "client_secret": "ps_01J9B…_secret_…",
  "url": "https://urdume.site/embed/01J9B…",
  "expires": 1790003600
}

2. Na sua página: embutir o componente

<div id="urdume"></div>
<script src="https://urdume.site/assets/js/urdume-embed.js"></script>
<script>
  // "session" é a resposta do passo 1, entregue pelo seu servidor a esta página
  Urdume.mount('#urdume', { url: session.url, clientSecret: session.client_secret });
</script>
ParâmetroDescrição
product O produto a mostrar (obrigatório).
origin A origem da página que embute o componente, por exemplo https://erp.a-sua-empresa.pt (obrigatório; HTTPS, sem caminho). O componente só abre dentro de páginas dessa origem.
allow_upload Opcional, por omissão true. Com false, o componente só mostra: não deixa enviar documentos.
  • A sessão dura 60 minutos e serve só para esse produto. Crie uma nova em cada carregamento da sua página.
  • Crie a sessão sempre no servidor: a chave sk_… nunca pode chegar ao browser. O client_secret pode, porque só dá acesso a este produto e por pouco tempo.
  • Sem o carregador, pode criar o iframe à mão com o endereço "url" seguido de "#" e do client_secret.
  • O componente não valida sugestões da IA nem emite passaportes: esses passos exigem uma pessoa identificada e fazem-se no Urdume (o componente tem o link).
  • Os documentos enviados pelo componente ficam registados como vindos da API.

Modelos de campos

GET /templates devolve os modelos de campos disponíveis, com os grupos, as chaves, os tipos e as opções de cada campo. Use-o para saber que chaves pode enviar em "data" e que valores são aceites.

Webhooks

Em vez de perguntar à API se algo mudou, registe um endereço HTTPS no painel (Programadores › Webhooks) e o Urdume envia-lhe um pedido POST sempre que acontece um dos eventos que escolheu.

EventoQuandoObjeto em data.object
product.created Produto criado product
product.updated Produto alterado, arquivado ou reativado product
batch.created Lote criado batch
batch.updated Valores do lote alterados batch
document.extracted A extração com IA terminou document
document.extraction_failed A extração falhou document
document.validated Os dados de um documento foram validados por uma pessoa document
data_request.answered Um fornecedor respondeu a um pedido de dados data_request
passport.issued Passaporte emitido passport
passport.version_published Nova versão de um passaporte passport
passport.withdrawn Passaporte retirado passport
passport.items_created Artigos (números de série) criados num passaporte passport
item.voided Artigo anulado item
lifecycle_event.recorded Evento pós-venda registado (publicado, ou por rever se vier de um parceiro ou de um consumidor) lifecycle_event
lifecycle_event.published Evento de um parceiro aprovado lifecycle_event
lifecycle_event.rejected Evento de um parceiro rejeitado lifecycle_event
lifecycle_event.voided Evento anulado lifecycle_event
passport_session.completed Uma sessão alojada foi concluída: o passaporte foi emitido ou atualizado na página alojada passport_session

O que o seu sistema recebe

POST /webhooks/urdume HTTP/1.1
Content-Type: application/json; charset=utf-8
User-Agent: Urdume-Webhooks/1.0
Urdume-Signature: t=1790000000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
Urdume-Event-Id: 01J8ZK3V6W9QX4T2M7N5B1C0DE
Urdume-Event-Type: passport.issued
Urdume-Delivery-Id: 01J8ZK3V7A2B3C4D5E6F7G8H9J

{
  "id": "01J8ZK3V6W9QX4T2M7N5B1C0DE",
  "object": "event",
  "type": "passport.issued",
  "api_version": "v1",
  "created": 1790000000,
  "livemode": true,
  "data": { "object": { "id": "01J8Z…", "object": "passport", … } }
}

Confirme sempre a assinatura antes de confiar no conteúdo. O cabeçalho Urdume-Signature traz o momento do envio (t) e a assinatura (v1): o HMAC-SHA256, com o segredo do endereço (whsec_…), do texto "t" + "." + corpo do pedido tal como foi recebido.

Verificar a assinatura (PHP)

<?php
$secret = getenv('URDUME_WEBHOOK_SECRET');            // whsec_…
$body = file_get_contents('php://input');                 // o corpo exato, antes de qualquer json_decode
$header = $_SERVER['HTTP_URDUME_SIGNATURE'] ?? '';

$parts = [];
foreach (explode(',', $header) as $part) {
    [$key, $value] = array_pad(explode('=', $part, 2), 2, '');
    $parts[$key] = $value;
}
$timestamp = (int) ($parts['t'] ?? 0);
$expected = hash_hmac('sha256', $timestamp . '.' . $body, $secret);

if (abs(time() - $timestamp) > 300 || !hash_equals($expected, $parts['v1'] ?? '')) {
    http_response_code(400);                               // assinatura inválida ou pedido antigo
    exit;
}

$event = json_decode($body, true);
// … guardar $event['id'] e tratar o evento (de preferência em segundo plano) …
http_response_code(200);

Verificar a assinatura (Node.js)

const crypto = require('crypto');

// body: o corpo em bruto (Buffer ou string), não o objeto já interpretado
function verify(body, header, secret) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  const expected = crypto.createHmac('sha256', secret).update(`${parts.t}.${body}`).digest('hex');
  const recent = Math.abs(Date.now() / 1000 - Number(parts.t)) <= 300;
  const a = Buffer.from(expected);
  const b = Buffer.from(parts.v1 || '');
  return recent && a.length === b.length && crypto.timingSafeEqual(a, b);
}
  • Responda com um código 2xx em menos de 10 segundos. Trate o evento depois de responder, se demorar.
  • Qualquer outra resposta (incluindo redirecionamentos, que não são seguidos) conta como falha. Repetimos até 8 vezes, com intervalos crescentes: 1 min, 5 min, 30 min, 2 h, 5 h, 10 h e 24 h.
  • O mesmo evento pode chegar mais do que uma vez: guarde o "id" dos eventos já tratados e ignore as repetições.
  • A ordem de chegada não é garantida. Se precisar do estado atual, leia o objeto pela API.
  • Recuse pedidos com o momento (t) a mais de 5 minutos da hora atual: impede que um pedido capturado seja reenviado mais tarde.
  • O endereço tem de ser HTTPS e público. Endereços de redes internas são recusados.
  • Se muitas entregas seguidas esgotarem todas as tentativas, o endereço é desligado e fica assinalado no painel. No painel pode ver cada entrega, reenviá-la e enviar um evento de teste ("ping").
  • Os endereços de produção só recebem eventos dos dados reais; os de teste, dos dados de teste.