Sourevo
Sviluppatori

API per collegare il tuo ERP

Tutto ciò che fa l'app Sourevo, senza schermo: il tuo ERP registra prodotti e fornitori, carica gli appezzamenti, trasforma ogni ricevimento merce in un lotto, prepara la dichiarazione (DDS), la presenta in TRACES e riceve il numero di riferimento via webhook.

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

Il flusso, passo dopo passo

Pensato per il modulo Odoo, vale per qualsiasi ERP o software di spedizionieri:

  1. Prodotti: registra ogni articolo EUDR con il suo codice NC (e il nome scientifico se è legno).
  2. Fornitori: sincronizza i tuoi contatti con il loro externalRef e invitali al loro portale; segui il loro livello di fiducia.
  3. Appezzamenti: il fornitore li carica nel portale, oppure carichi tu quelli che ti ha inviato come proposta da confermare.
  4. Lotto: ogni ricevimento merce dell'ERP è un lotto con la sua quantità e i suoi appezzamenti di origine.
  5. DDS: crea la bozza dal lotto e avvia il controllo preliminare (blocchi e avvisi).
  6. Presentazione: accetta gli avvisi e presenta in TRACES.
  7. Riferimento: TRACES restituisce il numero di riferimento e il webhook dds.status_changed lo porta nel tuo ERP.

Piani e modalità simulata

L'API è inclusa nei piani Pro, Impresa e Spedizionieri, senza costi aggiuntivi. I Pack non la includono: la chiamata risponde 403 plan_not_allowed.

Durante la prova di 14 giorni le chiavi funzionano in modalità simulata: le DDS vanno al TRACES simulato di Sourevo e ricevono riferimenti che iniziano con SIM. Non arrivano mai alla Commissione.

Ogni risposta riporta l'intestazione X-Sourevo-Mode (live o simulated) e GET /me restituisce modalità e piano. Ogni DDS riporta simulated: true o false.

Quando la prova o il piano terminano, le chiavi rispondono 403 plan_not_allowed con details.reason trial_expired o plan_expired.

Autenticazione

Ogni richiesta porta l'intestazione Authorization: Bearer svk_... con una chiave della tua organizzazione.

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

Le chiavi si creano nell'app, in API e integrazioni, solo da proprietari e amministratori. Si mostrano una sola volta: conserviamo solo la loro impronta.

La chiave agisce per conto di chi l'ha creata. Se questa persona lascia l'organizzazione o non è più amministratrice, la chiave risponde 403 key_owner_forbidden. Il primo utilizzo di ogni chiave viene segnalato via e-mail.

Facoltativo: l'intestazione X-Sourevo-Client (ad esempio "Odoo 18") indica quale programma chiama; compare nell'avviso di primo utilizzo.

Limite di richieste

Fino a 120 richieste al minuto per chiave. Ogni risposta riporta X-RateLimit-Limit e X-RateLimit-Remaining; oltre, 429 rate_limited con Retry-After in secondi.

Errori

Tutti gli errori hanno la stessa forma, in inglese: { "error": { "code", "message", "details" } }. Programma su code e details.reason, mai sul testo di 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..." }]
    }
  }
}
HTTPcodeSignificato
400validation_errorRichiesta malformata o campo non valido (vedi details).
400invalid_webhook_urlL'URL del webhook non è https o punta a una rete interna.
401missing_api_keyManca l'intestazione Authorization con una chiave.
401invalid_api_keyLa chiave non esiste o è stata revocata.
403plan_not_allowedIl piano non include l'API (Pack), oppure la prova o il piano sono terminati.
403key_owner_forbiddenChi ha creato la chiave ha lasciato l'organizzazione o non è più proprietario né amministratore.
403forbiddenIl ruolo della chiave non lo consente, oppure la risorsa appartiene a un'altra organizzazione.
404not_foundNon esiste nella tua organizzazione.
409conflictLo stato della risorsa non lo consente (vedi details.reason).
409idempotency_in_progressUn'altra richiesta con la stessa Idempotency-Key è ancora in corso.
413payload_too_largeIl corpo supera 2 MB.
422idempotency_key_reusedQuesta Idempotency-Key è già stata usata con un'altra richiesta.
422unprocessableUna regola di business lo impedisce (vedi details.reason).
429rate_limitedPiù di 120 richieste al minuto con questa chiave.
500internal_errorErrore imprevisto da parte nostra; viene registrato.
502upstream_errorImpossibile contattare TRACES. Riprova con la stessa Idempotency-Key.
503service_unavailableUn servizio facoltativo (controllo satellitare) non è configurato.

Motivi (details.reason)

Le regole di business rispondono 422 unprocessable (o 409 conflict, 400 validation_error) con un details.reason di questo elenco chiuso, in snake_case, e i dati per comporre il tuo messaggio. Ad esempio, pre_submission_blocked contiene blocking e warnings, e traces_rejected contiene i codici della Commissione in 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

Idempotenza

Ogni POST accetta l'intestazione Idempotency-Key (fino a 255 caratteri). Usala sempre quando presenti: se la connessione cade, riprova con la stessa chiave senza rischio di presentare due volte.

Stessa chiave e stessa richiesta entro 24 ore: restituiamo la risposta salvata con l'intestazione Idempotent-Replayed: true, senza ripetere nulla.

Stessa chiave con un altro corpo o percorso: 422 idempotency_key_reused.

Finché la prima è in corso: 409 idempotency_in_progress. Le risposte 5xx non vengono salvate: il nuovo tentativo viene eseguito di nuovo.

Paginazione e riferimenti del tuo ERP

Gli elenchi restituiscono { "data": [...], "nextCursor": "..." }. Chiedi la pagina successiva con ?cursor=<nextCursor>; limit va da 1 a 100 (50 di default). Nell'ultima pagina nextCursor è null.

Fornitori, prodotti e lotti accettano externalRef, l'id nel tuo ERP (ad esempio il ricevimento WH/IN/00012). Un POST con un externalRef già esistente non crea nulla: restituisce quello esistente con 200 e duplicate: true. Puoi cercare con ?externalRef=.

GET /dds filtra per status, operatorId, batchId, internalReferenceNumber e updatedSince (per sincronizzare tramite polling se non usi i webhook).

Endpoint

Base: /api/v1. Il riferimento completo, con tutti i campi e gli schemi, è nel documento OpenAPI.

MetodoPercorsoCosa fa
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

Webhook

Il tuo ERP registra il suo URL con POST /webhooks (https, mai una rete interna) e riceve il segreto una sola volta. Ogni avviso è un POST con l'evento in JSON.

Eventi

type
dds.submittedUna DDS è stata presentata in TRACES (dal web, dall'API o dalla sincronizzazione).
dds.status_changedUna DDS cambia stato: riferimento ricevuto (AVAILABLE), respinta, ritirata o modificata.
plot.proposal_answeredIl fornitore ha confermato, corretto o respinto un appezzamento che hai caricato come proposta.
plot.satellite_flaggedUn controllo satellitare ha rilevato perdita di copertura arborea: va verificato.
supplier.claimedIl fornitore ha creato il suo account e ha rivendicato la scheda che avevi registrato.
plan.quota_80Hai usato l'80 % delle dichiarazioni incluse nel tuo piano.
plan.quota_100Hai usato tutte le dichiarazioni incluse; le successive sono extra.

POST /webhooks/{id}/test invia un evento ping firmato per provare la connessione.

Corpo di ogni avviso: { "id", "type", "createdAt", "data" }. data contiene l'oggetto nella stessa forma dell'API (dds, plot, supplier...). Intestazioni: X-Sourevo-Event, X-Sourevo-Delivery (id della consegna) 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 }
  }
}

Firma: X-Sourevo-Signature: t=<unix>,v1=<hex>, dove v1 è l'HMAC-SHA256 con il tuo segreto di "<t>.<corpo così com'è>". Rifiuta gli avvisi con più di 5 minuti di differenza.

Qualsiasi risposta 2xx conferma la consegna. Altrimenti riproviamo dopo 1 min, 5 min, 30 min, 2 h, 6 h, 12 h e 24 h; poi la consegna risulta failed. Senza reindirizzamenti e con 10 secondi di attesa.

GET /webhooks/{id}/deliveries restituisce il registro delle consegne con tentativi, ultimo codice HTTP ed errore.

Verificare la firma

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", ""))

Esempi con curl

Sostituisci svk_... con la tua chiave. Gli id provengono dalle risposte precedenti.

Provare la connessione
curl https://sourevo.com/api/v1/me \
  -H "Authorization: Bearer svk_..." \
  -H "X-Sourevo-Client: Odoo 18"
Creare un prodotto con il codice 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"}'
Creare un fornitore
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"}'
Caricare un appezzamento come 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}'
Creare un lotto da un ricevimento
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"]}'
Creare la bozza di 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."}}'
Controllo preliminare
curl https://sourevo.com/api/v1/dds/DDS_ID/check \
  -H "Authorization: Bearer svk_..." \
  -H "X-Sourevo-Client: Odoo 18"
Presentare in 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"]}'
Registrare un 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"]}'
Stai iniziando un'integrazione?

Crea un account: la prova di 14 giorni include l'API in modalità simulata.

Crea un account gratuito