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:
- Producten: maak elk EUDR-artikel aan met zijn GN-code (en de wetenschappelijke naam bij hout).
- Leveranciers: synchroniseer je contacten met hun externalRef en nodig ze uit voor hun portaal; volg hun vertrouwensniveau.
- Percelen: de leverancier uploadt ze in zijn portaal, of jij uploadt de ontvangen percelen als voorstel dat hij bevestigt.
- Partij: elke goederenontvangst in het ERP is een partij met haar hoeveelheid en herkomstpercelen.
- DDS: maak het concept vanuit de partij en voer de voorafgaande controle uit (blokkades en waarschuwingen).
- Indiening: accepteer de waarschuwingen en dien in bij TRACES.
- 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 18Sleutels 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..." }]
}
}
}| HTTP | code | Betekenis |
|---|---|---|
| 400 | validation_error | Verkeerd opgebouwd verzoek of ongeldig veld (zie details). |
| 400 | invalid_webhook_url | De webhook-URL is geen https of wijst naar een intern netwerk. |
| 401 | missing_api_key | De header Authorization met een sleutel ontbreekt. |
| 401 | invalid_api_key | De sleutel bestaat niet of is ingetrokken. |
| 403 | plan_not_allowed | Het abonnement bevat de API niet (Packs), of de proefperiode of het abonnement is afgelopen. |
| 403 | key_owner_forbidden | Wie de sleutel aanmaakte, heeft de organisatie verlaten of is geen eigenaar of beheerder meer. |
| 403 | forbidden | De rol achter de sleutel staat dit niet toe, of de resource hoort bij een andere organisatie. |
| 404 | not_found | Bestaat niet in je organisatie. |
| 409 | conflict | De status van de resource staat het niet toe (zie details.reason). |
| 409 | idempotency_in_progress | Een ander verzoek met dezelfde Idempotency-Key loopt nog. |
| 413 | payload_too_large | De body is groter dan 2 MB. |
| 422 | idempotency_key_reused | Deze Idempotency-Key is al gebruikt voor een ander verzoek. |
| 422 | unprocessable | Een bedrijfsregel verhindert het (zie details.reason). |
| 429 | rate_limited | Meer dan 120 verzoeken per minuut met deze sleutel. |
| 500 | internal_error | Onverwachte fout aan onze kant; die wordt gelogd. |
| 502 | upstream_error | TRACES was niet bereikbaar. Probeer opnieuw met dezelfde Idempotency-Key. |
| 503 | service_unavailable | Een 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.
| Methode | Pad | Wat het doet |
|---|---|---|
| GET | /me | Organization, plan, mode and key (use it to test the connection) |
| GET | /operators | Companies that submit to TRACES |
| GET | /suppliers | List suppliers |
| POST | /suppliers | Create a supplier |
| GET | /suppliers/{id} | Get a supplier |
| PATCH | /suppliers/{id} | Update a supplier you own (partial) |
| GET | /suppliers/{id}/status | Trust level, plots summary, account and invitation |
| POST | /suppliers/{id}/invitations | Invite the supplier to its portal (emailed when it has an address) |
| GET | /suppliers/{id}/plots | Plots of a supplier your organization can use |
| POST | /suppliers/{id}/plots | Upload plots as your proposal to the supplier |
| GET | /plots/{id} | Get a plot |
| POST | /plots/{id}/verify | Verify (approve) the plot for your organization |
| POST | /plots/{id}/correction-request | Ask the supplier to correct the plot ({ reason }) |
| POST | /plots/{id}/satellite-checks | Run a satellite deforestation check now |
| GET | /plots/{id}/satellite-checks/latest | Latest satellite check by your organization |
| GET | /products | List products |
| POST | /products | Create a product (CN code; scientific name for wood) |
| GET | /products/{id} | Get a product |
| GET | /batches | List batches |
| POST | /batches | Create a batch (a goods receipt) and link its plots |
| GET | /batches/{id} | Get a batch |
| POST | /batches/{id}/plots | Link plots ({ plotIds }) |
| DELETE | /batches/{id}/plots/{plotId} | Unlink a plot |
| GET | /dds | List due diligence statements |
| POST | /dds | Create 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-assessment | Record the risk assessment of a draft |
| GET | /dds/{id}/check | Pre-submission check: blocking issues and warnings |
| POST | /dds/{id}/submit | Submit to TRACES ({ acknowledgedWarnings }) |
| POST | /dds/{id}/amend | Amend a submitted DDS (activityType, countries, comment, geoLocationConfidential) |
| POST | /dds/{id}/retract | Retract a submitted DDS ({ reason }) |
| POST | /dds/{id}/refresh | Ask TRACES for the current status now |
| GET | /webhooks | List webhook endpoints |
| POST | /webhooks | Register a webhook endpoint ({ url, events?, description? }) |
| DELETE | /webhooks/{id} | Delete a webhook endpoint |
| POST | /webhooks/{id}/test | Send a signed ping now |
| GET | /webhooks/{id}/deliveries | Delivery 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.submitted | Een DDS is ingediend bij TRACES (via het web, de API of de synchronisatie). |
dds.status_changed | Een DDS wijzigt van status: referentie ontvangen (AVAILABLE), afgewezen, ingetrokken of gewijzigd. |
plot.proposal_answered | De leverancier heeft een perceel dat jij voorstelde bevestigd, gecorrigeerd of afgewezen. |
plot.satellite_flagged | Een satellietcontrole heeft verlies van boombedekking gevonden: nakijken. |
supplier.claimed | De leverancier heeft zijn account aangemaakt en het record dat jij aanmaakte opgeëist. |
plan.quota_80 | Je hebt 80% van de verklaringen in je abonnement gebruikt. |
plan.quota_100 | Je 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
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'));
}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.
curl https://sourevo.com/api/v1/me \
-H "Authorization: Bearer svk_..." \
-H "X-Sourevo-Client: Odoo 18"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"}'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"}'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}'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"]}'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."}}'curl https://sourevo.com/api/v1/dds/DDS_ID/check \
-H "Authorization: Bearer svk_..." \
-H "X-Sourevo-Client: Odoo 18"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"]}'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"]}'Maak een account aan: de proefperiode van 14 dagen bevat de API in gesimuleerde modus.
Gratis account aanmaken