Sourevo
Programadores

API para ligar o seu ERP

Tudo o que a aplicação Sourevo faz, sem ecrã: o seu ERP regista produtos e fornecedores, envia parcelas, transforma cada receção de mercadoria num lote, prepara a declaração (DDS), submete-a no TRACES e recebe o número de referência por webhook.

Documento OpenAPI 3.1 · https://sourevo.com/api/v1

O fluxo, passo a passo

Pensado para o módulo de Odoo, serve para qualquer ERP ou software de transitário:

  1. Produtos: registe cada artigo RUED com o seu código NC (e o nome científico se for madeira).
  2. Fornecedores: sincronize os seus contactos com o externalRef e convide-os para o portal; acompanhe o nível de confiança.
  3. Parcelas: o fornecedor carrega-as no portal, ou envia as que ele lhe mandou como proposta para que as confirme.
  4. Lote: cada receção de mercadoria do ERP é um lote com a quantidade e as parcelas de origem.
  5. DDS: crie o rascunho a partir do lote e execute a verificação prévia (bloqueios e avisos).
  6. Submissão: aceite os avisos e submeta no TRACES.
  7. Referência: o TRACES devolve o número de referência e o webhook dds.status_changed leva-o ao seu ERP.

Planos e modo simulado

A API está incluída nos planos Pro, Empresa e Transitário, sem custo adicional. Os Packs não a incluem: a chamada responde 403 plan_not_allowed.

Durante o período de teste de 14 dias, as chaves funcionam em modo simulado: as DDS vão para o TRACES simulado da Sourevo e recebem referências que começam por SIM. Nunca chegam à Comissão.

Cada resposta traz o cabeçalho X-Sourevo-Mode (live ou simulated) e GET /me devolve o modo e o plano. Cada DDS indica simulated: true ou false.

Quando o teste ou o plano termina, as chaves respondem 403 plan_not_allowed com details.reason trial_expired ou plan_expired.

Autenticação

Cada pedido leva o cabeçalho Authorization: Bearer svk_... com uma chave da sua organização.

Authorization: Bearer svk_...
X-Sourevo-Client: Odoo 18

As chaves criam-se na aplicação, em API e integrações, apenas por proprietários e administradores. São mostradas uma só vez: guardamos apenas a impressão digital.

A chave atua em nome de quem a criou. Se essa pessoa sair da organização ou deixar de ser administradora, a chave responde 403 key_owner_forbidden. A primeira utilização de cada chave é avisada por e-mail.

Opcional: o cabeçalho X-Sourevo-Client (por exemplo "Odoo 18") indica que programa chama; aparece no aviso da primeira utilização.

Limite de pedidos

Até 120 pedidos por minuto e por chave. Cada resposta traz X-RateLimit-Limit e X-RateLimit-Remaining; acima disso, 429 rate_limited com Retry-After em segundos.

Erros

Todos os erros têm a mesma forma, em inglês: { "error": { "code", "message", "details" } }. Programe com base em code e details.reason, nunca no texto de message.

HTTP/1.1 422 Unprocessable Entity
{
  "error": {
    "code": "unprocessable",
    "message": "Every warning must be acknowledged: send their keys in acknowledgedWarnings.",
    "details": {
      "reason": "acknowledgement_required",
      "blocking": [],
      "warnings": [{ "key": "supplier:cm3s...:SUPPLIER_TRUST_BELOW_VALIDATED", "code": "SUPPLIER_TRUST_BELOW_VALIDATED", "level": "supplier", "entityId": "cm3s..." }]
    }
  }
}
HTTPcodeSignificado
400validation_errorPedido mal formado ou campo inválido (ver details).
400invalid_webhook_urlO URL do webhook não é https ou aponta para uma rede interna.
401missing_api_keyFalta o cabeçalho Authorization com uma chave.
401invalid_api_keyA chave não existe ou foi revogada.
403plan_not_allowedO plano não inclui a API (Packs), ou o teste ou o plano terminaram.
403key_owner_forbiddenQuem criou a chave saiu da organização ou já não é proprietário nem administrador.
403forbiddenA função da chave não permite esta ação, ou o recurso pertence a outra organização.
404not_foundNão existe na sua organização.
409conflictO estado do recurso não o permite (ver details.reason).
409idempotency_in_progressOutro pedido com a mesma Idempotency-Key ainda está em curso.
413payload_too_largeO corpo ultrapassa 2 MB.
422idempotency_key_reusedEssa Idempotency-Key já foi usada noutro pedido.
422unprocessableUma regra de negócio impede-o (ver details.reason).
429rate_limitedMais de 120 pedidos por minuto com esta chave.
500internal_errorErro inesperado do nosso lado; fica registado.
502upstream_errorNão foi possível contactar o TRACES. Tente novamente com a mesma Idempotency-Key.
503service_unavailableUm serviço opcional (verificação por satélite) não está configurado.

Motivos (details.reason)

As regras de negócio respondem 422 unprocessable (ou 409 conflict, 400 validation_error) com um details.reason desta lista fechada, em snake_case, e os dados de que precisa para montar a sua própria mensagem. Por exemplo, pre_submission_blocked traz blocking e warnings, e traces_rejected traz os códigos da Comissão em rules.

acknowledgement_requiredarea_required_for_pointbatch_frozenbatch_has_live_ddsbusiness_rulecoordinates_incompleteexternal_ref_takengeojson_emptygeojson_feature_without_geometrygeojson_invalid_geometrygeojson_invalid_jsongeojson_not_wgs84geojson_polygon_invalidgeojson_polygon_not_closedgeojson_polygon_too_few_verticesgeojson_too_many_verticesgeojson_unsupported_geometrygeometry_requiredinternal_reference_takeninvalid_addressinvalid_areainvalid_cn_codeinvalid_countryinvalid_cursorinvalid_dateinvalid_emailinvalid_external_refinvalid_idempotency_keyinvalid_inputinvalid_jsoninvalid_legal_nameinvalid_limitinvalid_nameinvalid_phoneinvalid_quantityinvalid_statusinvalid_tax_idinvalid_verificationkey_already_revokedlatitude_out_of_rangelongitude_out_of_rangeno_satellite_checknot_a_draftoperator_without_credentialspack_planplan_expiredplan_limit_reachedplot_not_foundplot_rejectedplot_without_geometrypolygon_required_over_4_hapre_submission_blockedquantity_requiredreason_requiredretract_reason_requiredrole_not_allowedscientific_name_requiredstorage_unavailablesubmission_in_progresssupplier_already_claimedsupplier_not_ownedtax_id_declared_by_suppliertoo_many_webhookstraces_faulttraces_permission_deniedtraces_rejectedtrial_expiredtrial_simulated_only

Idempotência

Todo o POST aceita o cabeçalho Idempotency-Key (até 255 caracteres). Use-o sempre ao submeter: se a ligação cair, repita com a mesma chave sem risco de submeter duas vezes.

Mesma chave e mesmo pedido durante 24 horas: devolvemos a resposta guardada com o cabeçalho Idempotent-Replayed: true, sem repetir nada.

Mesma chave com outro corpo ou caminho: 422 idempotency_key_reused.

Enquanto o primeiro estiver em curso: 409 idempotency_in_progress. As respostas 5xx não são guardadas: a repetição volta a ser executada.

Paginação e referências do seu ERP

As listas devolvem { "data": [...], "nextCursor": "..." }. Peça a página seguinte com ?cursor=<nextCursor>; limit vai de 1 a 100 (50 por omissão). Na última página, nextCursor é null.

Fornecedores, produtos e lotes aceitam externalRef, o id no seu ERP (por exemplo a receção WH/IN/00012). Um POST com um externalRef que já existe não cria nada: devolve o existente com 200 e duplicate: true. Pode pesquisar com ?externalRef=.

GET /dds filtra por status, operatorId, batchId, internalReferenceNumber e updatedSince (para sincronizar por consulta periódica se não usar webhooks).

Endpoints

Base: /api/v1. A referência completa, com todos os campos e esquemas, está no documento OpenAPI.

MétodoCaminhoO que faz
GET/meOrganization, plan, mode and key (use it to test the connection)
GET/operatorsCompanies that submit to TRACES
GET/suppliersList suppliers
POST/suppliersCreate a supplier
GET/suppliers/{id}Get a supplier
PATCH/suppliers/{id}Update a supplier you own (partial)
GET/suppliers/{id}/statusTrust level, plots summary, account and invitation
POST/suppliers/{id}/invitationsInvite the supplier to its portal (emailed when it has an address)
GET/suppliers/{id}/plotsPlots of a supplier your organization can use
POST/suppliers/{id}/plotsUpload plots as your proposal to the supplier
GET/plots/{id}Get a plot
POST/plots/{id}/verifyVerify (approve) the plot for your organization
POST/plots/{id}/correction-requestAsk the supplier to correct the plot ({ reason })
POST/plots/{id}/satellite-checksRun a satellite deforestation check now
GET/plots/{id}/satellite-checks/latestLatest satellite check by your organization
GET/productsList products
POST/productsCreate a product (CN code; scientific name for wood)
GET/products/{id}Get a product
GET/batchesList batches
POST/batchesCreate a batch (a goods receipt) and link its plots
GET/batches/{id}Get a batch
POST/batches/{id}/plotsLink plots ({ plotIds })
DELETE/batches/{id}/plots/{plotId}Unlink a plot
GET/ddsList due diligence statements
POST/ddsCreate a draft from a batch
GET/dds/{id}Get a DDS (status, reference and verification numbers)
PATCH/dds/{id}Change a draft
DELETE/dds/{id}Delete a draft
POST/dds/{id}/risk-assessmentRecord the risk assessment of a draft
GET/dds/{id}/checkPre-submission check: blocking issues and warnings
POST/dds/{id}/submitSubmit to TRACES ({ acknowledgedWarnings })
POST/dds/{id}/amendAmend a submitted DDS (activityType, countries, comment, geoLocationConfidential)
POST/dds/{id}/retractRetract a submitted DDS ({ reason })
POST/dds/{id}/refreshAsk TRACES for the current status now
GET/webhooksList webhook endpoints
POST/webhooksRegister a webhook endpoint ({ url, events?, description? })
DELETE/webhooks/{id}Delete a webhook endpoint
POST/webhooks/{id}/testSend a signed ping now
GET/webhooks/{id}/deliveriesDelivery log of an endpoint

Webhooks

O seu ERP regista o URL com POST /webhooks (https, nunca uma rede interna) e recebe o segredo uma única vez. Cada aviso é um POST com o evento em JSON.

Eventos

type
dds.submittedUma DDS foi submetida no TRACES (a partir da web, da API ou da sincronização).
dds.status_changedUma DDS muda de estado: referência recebida (AVAILABLE), rejeitada, retirada ou alterada.
plot.proposal_answeredO fornecedor confirmou, corrigiu ou rejeitou uma parcela que enviou como proposta.
plot.satellite_flaggedUma verificação por satélite detetou perda de coberto arbóreo: é preciso revê-la.
supplier.claimedO fornecedor criou a sua conta e reclamou a ficha que registou.
plan.quota_80Usou 80 % das declarações incluídas no seu plano.
plan.quota_100Usou todas as declarações incluídas; as seguintes são extra.

POST /webhooks/{id}/test envia um evento ping assinado para testar a ligação.

Corpo de cada aviso: { "id", "type", "createdAt", "data" }. data traz o objeto com a mesma forma que na API (dds, plot, supplier...). Cabeçalhos: X-Sourevo-Event, X-Sourevo-Delivery (id da entrega) e X-Sourevo-Signature.

{
  "id": "cm3x...",
  "type": "dds.status_changed",
  "createdAt": "2026-12-30T09:14:02.000Z",
  "data": {
    "previousStatus": "SUBMITTED",
    "status": "AVAILABLE",
    "dds": { "id": "cm3w...", "status": "AVAILABLE", "referenceNumber": "26ESXXXXXXXXXX", "verificationNumber": "XXXXXXXX", "simulated": false }
  }
}

Assinatura: X-Sourevo-Signature: t=<unix>,v1=<hex>, em que v1 é o HMAC-SHA256 com o seu segredo de "<t>.<corpo tal como chega>". Recuse avisos com mais de 5 minutos de diferença.

Qualquer resposta 2xx confirma a entrega. Caso contrário, repetimos após 1 min, 5 min, 30 min, 2 h, 6 h, 12 h e 24 h; depois, a entrega fica como failed. Sem redirecionamentos e com 10 segundos de espera.

GET /webhooks/{id}/deliveries devolve o registo de entregas com tentativas, último código HTTP e erro.

Verificar a assinatura

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

// rawBody: the request body exactly as received (string).
export function isFromSourevo(secret, rawBody, header) {
  const parts = Object.fromEntries(header.split(',').map((x) => x.split('=')));
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false;
  const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
  const given = Buffer.from(parts.v1 ?? '', 'hex');
  return given.length === 32 && timingSafeEqual(given, Buffer.from(expected, 'hex'));
}
Python
import hashlib, hmac, time

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

Exemplos com curl

Substitua svk_... pela sua chave. Os ids vêm das respostas anteriores.

Testar a ligação
curl https://sourevo.com/api/v1/me \
  -H "Authorization: Bearer svk_..." \
  -H "X-Sourevo-Client: Odoo 18"
Criar um produto com o código NC
curl -X POST https://sourevo.com/api/v1/products \
  -H "Authorization: Bearer svk_..." \
  -H "X-Sourevo-Client: Odoo 18" \
  -H "Content-Type: application/json" \
  -d '{"description":"Green coffee","cnCode":"0901 11 00","externalRef":"product_42"}'
Criar um fornecedor
curl -X POST https://sourevo.com/api/v1/suppliers \
  -H "Authorization: Bearer svk_..." \
  -H "X-Sourevo-Client: Odoo 18" \
  -H "Content-Type: application/json" \
  -d '{"name":"Finca La Esperanza","legalName":"Finca La Esperanza S.A.S.","country":"CO","taxId":"900123456","email":"finca@example.com","postalAddress":"Vereda El Placer, Pitalito, Huila","externalRef":"res.partner_7"}'
Enviar uma parcela como proposta
curl -X POST https://sourevo.com/api/v1/suppliers/SUPPLIER_ID/plots \
  -H "Authorization: Bearer svk_..." \
  -H "X-Sourevo-Client: Odoo 18" \
  -H "Content-Type: application/json" \
  -d '{"name":"Lote 1","country":"CO","areaHa":2.5,"latitude":1.85,"longitude":-76.05}'
Criar um lote a partir de uma receção
curl -X POST https://sourevo.com/api/v1/batches \
  -H "Authorization: Bearer svk_..." \
  -H "X-Sourevo-Client: Odoo 18" \
  -H "Content-Type: application/json" \
  -d '{"productId":"PRODUCT_ID","netWeightKg":18000,"externalRef":"WH/IN/00012","plotIds":["PLOT_ID"]}'
Criar o rascunho de DDS
curl -X POST https://sourevo.com/api/v1/dds \
  -H "Authorization: Bearer svk_..." \
  -H "X-Sourevo-Client: Odoo 18" \
  -H "Content-Type: application/json" \
  -d '{"batchId":"BATCH_ID","operatorId":"OPERATOR_ID","internalReferenceNumber":"WH-IN-00012","activityType":"IMPORT","operatorRole":"OPERATOR","countryOfActivity":"ES","riskAssessment":{"conclusion":"NEGLIGIBLE","notes":"Verified plots, satellite check without loss."}}'
Verificação prévia
curl https://sourevo.com/api/v1/dds/DDS_ID/check \
  -H "Authorization: Bearer svk_..." \
  -H "X-Sourevo-Client: Odoo 18"
Submeter no TRACES
curl -X POST https://sourevo.com/api/v1/dds/DDS_ID/submit \
  -H "Authorization: Bearer svk_..." \
  -H "X-Sourevo-Client: Odoo 18" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: submit-WH-IN-00012" \
  -d '{"acknowledgedWarnings":["WARNING_KEY"]}'
Registar um webhook
curl -X POST https://sourevo.com/api/v1/webhooks \
  -H "Authorization: Bearer svk_..." \
  -H "X-Sourevo-Client: Odoo 18" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://erp.example.com/sourevo/webhook","events":["dds.status_changed","plot.proposal_answered"]}'
Vai começar a integrar?

Crie uma conta: o teste de 14 dias inclui a API em modo simulado.

Criar conta grátis