Sourevo
Ontwikkelaars

API om je ERP te koppelen

Alles wat de Sourevo-app doet, zonder scherm: je ERP maakt producten en leveranciers aan, uploadt percelen, maakt van elke goederenontvangst een partij, bereidt de verklaring (DDS) voor, dient haar in bij TRACES en krijgt het referentienummer terug via een webhook.

OpenAPI 3.1-document · https://sourevo.com/api/v1

De stroom, stap voor stap

Ontworpen voor de Odoo-module, geldig voor elk ERP of expeditiesoftware:

  1. Producten: maak elk EUDR-artikel aan met zijn GN-code (en de wetenschappelijke naam bij hout).
  2. Leveranciers: synchroniseer je contacten met hun externalRef en nodig ze uit voor hun portaal; volg hun vertrouwensniveau.
  3. Percelen: de leverancier uploadt ze in zijn portaal, of jij uploadt de ontvangen percelen als voorstel dat hij bevestigt.
  4. Partij: elke goederenontvangst in het ERP is een partij met haar hoeveelheid en herkomstpercelen.
  5. DDS: maak het concept vanuit de partij en voer de voorafgaande controle uit (blokkades en waarschuwingen).
  6. Indiening: accepteer de waarschuwingen en dien in bij TRACES.
  7. Referentie: TRACES geeft het referentienummer terug en de webhook dds.status_changed brengt het naar je ERP.

Abonnementen en gesimuleerde modus

De API is zonder meerprijs inbegrepen in de abonnementen Pro, Onderneming en Expediteur. Packs bevatten haar niet: oproepen antwoorden 403 plan_not_allowed.

Tijdens de proefperiode van 14 dagen werken sleutels in gesimuleerde modus: verklaringen gaan naar het gesimuleerde TRACES van Sourevo en krijgen referenties die met SIM beginnen. Ze bereiken de Commissie nooit.

Elk antwoord bevat de header X-Sourevo-Mode (live of simulated) en GET /me geeft de modus en het abonnement. Elke DDS vermeldt simulated: true of false.

Als de proefperiode of het abonnement afloopt, antwoorden sleutels 403 plan_not_allowed met details.reason trial_expired of plan_expired.

Authenticatie

Elk verzoek bevat de header Authorization: Bearer svk_... met een sleutel van je organisatie.

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

Sleutels worden in de app aangemaakt, onder API en integraties, alleen door eigenaren en beheerders. Ze worden één keer getoond: we bewaren alleen hun vingerafdruk.

Een sleutel handelt namens de persoon die hem aanmaakte. Verlaat die de organisatie of is die geen beheerder meer, dan antwoordt de sleutel 403 key_owner_forbidden. Het eerste gebruik van elke sleutel wordt per e-mail gemeld.

Optioneel: de header X-Sourevo-Client (bijvoorbeeld "Odoo 18") zegt welk programma belt; hij staat in de e-mail over het eerste gebruik.

Verzoeklimiet

Tot 120 verzoeken per minuut per sleutel. Elk antwoord bevat X-RateLimit-Limit en X-RateLimit-Remaining; daarboven 429 rate_limited met Retry-After in seconden.

Fouten

Alle fouten hebben dezelfde vorm, in het Engels: { "error": { "code", "message", "details" } }. Programmeer op code en details.reason, nooit op de tekst van 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..." }]
    }
  }
}
HTTPcodeBetekenis
400validation_errorVerkeerd opgebouwd verzoek of ongeldig veld (zie details).
400invalid_webhook_urlDe webhook-URL is geen https of wijst naar een intern netwerk.
401missing_api_keyDe header Authorization met een sleutel ontbreekt.
401invalid_api_keyDe sleutel bestaat niet of is ingetrokken.
403plan_not_allowedHet abonnement bevat de API niet (Packs), of de proefperiode of het abonnement is afgelopen.
403key_owner_forbiddenWie de sleutel aanmaakte, heeft de organisatie verlaten of is geen eigenaar of beheerder meer.
403forbiddenDe rol achter de sleutel staat dit niet toe, of de resource hoort bij een andere organisatie.
404not_foundBestaat niet in je organisatie.
409conflictDe status van de resource staat het niet toe (zie details.reason).
409idempotency_in_progressEen ander verzoek met dezelfde Idempotency-Key loopt nog.
413payload_too_largeDe body is groter dan 2 MB.
422idempotency_key_reusedDeze Idempotency-Key is al gebruikt voor een ander verzoek.
422unprocessableEen bedrijfsregel verhindert het (zie details.reason).
429rate_limitedMeer dan 120 verzoeken per minuut met deze sleutel.
500internal_errorOnverwachte fout aan onze kant; die wordt gelogd.
502upstream_errorTRACES was niet bereikbaar. Probeer opnieuw met dezelfde Idempotency-Key.
503service_unavailableEen optionele dienst (satellietcontrole) is niet geconfigureerd.

Redenen (details.reason)

Bedrijfsregels antwoorden 422 unprocessable (of 409 conflict, 400 validation_error) met een details.reason uit deze gesloten lijst, in snake_case, plus de gegevens om je eigen melding op te bouwen. Zo bevat pre_submission_blocked blocking en warnings, en bevat traces_rejected de codes van de Commissie 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

Idempotentie

Elke POST accepteert de header Idempotency-Key (tot 255 tekens). Gebruik hem altijd bij indienen: valt de verbinding weg, probeer dan opnieuw met dezelfde sleutel zonder dubbel in te dienen.

Zelfde sleutel en zelfde verzoek binnen 24 uur: we geven het opgeslagen antwoord terug met de header Idempotent-Replayed: true, zonder iets te herhalen.

Zelfde sleutel met een andere body of pad: 422 idempotency_key_reused.

Zolang het eerste nog loopt: 409 idempotency_in_progress. 5xx-antwoorden worden niet opgeslagen: de nieuwe poging wordt opnieuw uitgevoerd.

Paginering en referenties van je ERP

Lijsten geven { "data": [...], "nextCursor": "..." }. Vraag de volgende pagina op met ?cursor=<nextCursor>; limit loopt van 1 tot 100 (standaard 50). Op de laatste pagina is nextCursor null.

Leveranciers, producten en partijen accepteren externalRef, de id in je ERP (bijvoorbeeld de ontvangst WH/IN/00012). Een POST met een bestaande externalRef maakt niets aan: hij geeft het bestaande record terug met 200 en duplicate: true. Zoeken kan met ?externalRef=.

GET /dds filtert op status, operatorId, batchId, internalReferenceNumber en updatedSince (om via polling te synchroniseren als je geen webhooks gebruikt).

Endpoints

Basis: /api/v1. De volledige referentie, met alle velden en schema's, staat in het OpenAPI-document.

MethodePadWat het doet
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

Je ERP registreert zijn URL met POST /webhooks (https, nooit een intern netwerk) en krijgt het geheim één keer. Elke melding is een POST met de gebeurtenis in JSON.

Gebeurtenissen

type
dds.submittedEen DDS is ingediend bij TRACES (via het web, de API of de synchronisatie).
dds.status_changedEen DDS wijzigt van status: referentie ontvangen (AVAILABLE), afgewezen, ingetrokken of gewijzigd.
plot.proposal_answeredDe leverancier heeft een perceel dat jij voorstelde bevestigd, gecorrigeerd of afgewezen.
plot.satellite_flaggedEen satellietcontrole heeft verlies van boombedekking gevonden: nakijken.
supplier.claimedDe leverancier heeft zijn account aangemaakt en het record dat jij aanmaakte opgeëist.
plan.quota_80Je hebt 80% van de verklaringen in je abonnement gebruikt.
plan.quota_100Je hebt alle inbegrepen verklaringen gebruikt; de volgende zijn extra.

POST /webhooks/{id}/test stuurt een ondertekende ping om de verbinding te testen.

Body van elke melding: { "id", "type", "createdAt", "data" }. data bevat het object in dezelfde vorm als in de API (dds, plot, supplier...). Headers: X-Sourevo-Event, X-Sourevo-Delivery (id van de levering) en 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 }
  }
}

Handtekening: X-Sourevo-Signature: t=<unix>,v1=<hex>, waarbij v1 de HMAC-SHA256 met je geheim is over "<t>.<ruwe body>". Weiger meldingen die meer dan 5 minuten afwijken.

Elk 2xx-antwoord bevestigt de levering. Anders proberen we opnieuw na 1 min, 5 min, 30 min, 2 u, 6 u, 12 u en 24 u; daarna staat de levering op failed. Zonder doorverwijzingen en met een time-out van 10 seconden.

GET /webhooks/{id}/deliveries geeft het leveringslogboek met pogingen, laatste HTTP-status en fout.

De handtekening controleren

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

curl-voorbeelden

Vervang svk_... door je sleutel. De id's komen uit de vorige antwoorden.

De verbinding testen
curl https://sourevo.com/api/v1/me \
  -H "Authorization: Bearer svk_..." \
  -H "X-Sourevo-Client: Odoo 18"
Een product met GN-code aanmaken
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"}'
Een leverancier aanmaken
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"}'
Een perceel als voorstel uploaden
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}'
Een partij uit een ontvangst aanmaken
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"]}'
Het DDS-concept aanmaken
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."}}'
Voorafgaande controle
curl https://sourevo.com/api/v1/dds/DDS_ID/check \
  -H "Authorization: Bearer svk_..." \
  -H "X-Sourevo-Client: Odoo 18"
Indienen bij 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"]}'
Een webhook registreren
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"]}'
Begin je met een integratie?

Maak een account aan: de proefperiode van 14 dagen bevat de API in gesimuleerde modus.

Gratis account aanmaken