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"
}
}
| HTTP | type | Quando |
|---|---|---|
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".
| Pedido | O 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âmetro | Descriçã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.
| Pedido | O 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.
| Pedido | O 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.
| Pedido | O 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. |
| Campo | Descriçã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.
| Pedido | O 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âmetro | Descriçã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.
| Pedido | O 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âmetro | Descriçã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âmetro | Descriçã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âmetro | Descriçã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.
| Evento | Quando | Objeto 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.