API pour connecter votre ERP
Tout ce que fait l'application Sourevo, sans écran : votre ERP crée produits et fournisseurs, envoie les parcelles, transforme chaque réception de marchandise en lot, prépare la déclaration (DDS), la dépose dans TRACES et reçoit le numéro de référence par webhook.
Document OpenAPI 3.1 · https://sourevo.com/api/v1
Le parcours, étape par étape
Conçu pour le module Odoo, valable pour tout ERP ou logiciel de transitaire :
- Produits : créez chaque article RDUE avec son code NC (et le nom scientifique pour le bois).
- Fournisseurs : synchronisez vos contacts avec leur externalRef et invitez-les sur leur portail ; suivez leur niveau de confiance.
- Parcelles : le fournisseur les dépose sur son portail, ou vous envoyez celles qu'il vous a transmises comme proposition à confirmer.
- Lot : chaque réception de marchandise de l'ERP est un lot avec sa quantité et ses parcelles d'origine.
- DDS : créez le brouillon à partir du lot et lancez le contrôle préalable (blocages et avertissements).
- Dépôt : acceptez les avertissements et déposez dans TRACES.
- Référence : TRACES renvoie le numéro de référence et le webhook dds.status_changed le transmet à votre ERP.
Offres et mode simulé
L'API est incluse dans les offres Pro, Entreprise et Transitaire, sans surcoût. Les Packs ne l'incluent pas : l'appel répond 403 plan_not_allowed.
Pendant l'essai de 14 jours, les clés fonctionnent en mode simulé : les DDS vont au TRACES simulé de Sourevo et reçoivent des références commençant par SIM. Elles n'atteignent jamais la Commission.
Chaque réponse porte l'en-tête X-Sourevo-Mode (live ou simulated) et GET /me renvoie le mode et l'offre. Chaque DDS indique simulated: true ou false.
Quand l'essai ou l'offre se termine, les clés répondent 403 plan_not_allowed avec details.reason trial_expired ou plan_expired.
Authentification
Chaque requête porte l'en-tête Authorization: Bearer svk_... avec une clé de votre organisation.
Authorization: Bearer svk_...
X-Sourevo-Client: Odoo 18Les clés se créent dans l'application, sous API et intégrations, uniquement par les propriétaires et administrateurs. Elles ne s'affichent qu'une fois : nous ne gardons que leur empreinte.
La clé agit au nom de la personne qui l'a créée. Si elle quitte l'organisation ou n'est plus administratrice, la clé répond 403 key_owner_forbidden. La première utilisation de chaque clé est signalée par e-mail.
Facultatif : l'en-tête X-Sourevo-Client (par exemple "Odoo 18") indique le programme appelant ; il apparaît dans l'e-mail de première utilisation.
Limite de requêtes
Jusqu'à 120 requêtes par minute et par clé. Chaque réponse porte X-RateLimit-Limit et X-RateLimit-Remaining ; au-delà, 429 rate_limited avec Retry-After en secondes.
Erreurs
Toutes les erreurs ont la même forme, en anglais : { "error": { "code", "message", "details" } }. Programmez sur code et details.reason, jamais sur le texte 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..." }]
}
}
}| HTTP | code | Signification |
|---|---|---|
| 400 | validation_error | Requête mal formée ou champ invalide (voir details). |
| 400 | invalid_webhook_url | L'URL du webhook n'est pas en https ou pointe vers un réseau interne. |
| 401 | missing_api_key | L'en-tête Authorization avec une clé est absent. |
| 401 | invalid_api_key | La clé n'existe pas ou a été révoquée. |
| 403 | plan_not_allowed | L'offre n'inclut pas l'API (Packs), ou l'essai ou l'offre est terminé. |
| 403 | key_owner_forbidden | La personne qui a créé la clé a quitté l'organisation ou n'est plus propriétaire ni administratrice. |
| 403 | forbidden | Le rôle de la clé ne permet pas cette action, ou la ressource appartient à une autre organisation. |
| 404 | not_found | N'existe pas dans votre organisation. |
| 409 | conflict | L'état de la ressource ne le permet pas (voir details.reason). |
| 409 | idempotency_in_progress | Une autre requête avec la même Idempotency-Key est encore en cours. |
| 413 | payload_too_large | Le corps dépasse 2 Mo. |
| 422 | idempotency_key_reused | Cette Idempotency-Key a déjà servi pour une autre requête. |
| 422 | unprocessable | Une règle métier l'empêche (voir details.reason). |
| 429 | rate_limited | Plus de 120 requêtes par minute avec cette clé. |
| 500 | internal_error | Erreur inattendue de notre côté ; elle est enregistrée. |
| 502 | upstream_error | TRACES n'a pas pu être joint. Réessayez avec la même Idempotency-Key. |
| 503 | service_unavailable | Un service facultatif (contrôle par satellite) n'est pas configuré. |
Motifs (details.reason)
Les règles métier répondent 422 unprocessable (ou 409 conflict, 400 validation_error) avec un details.reason issu de cette liste fermée, en snake_case, et les données utiles pour composer votre propre message. Par exemple, pre_submission_blocked contient blocking et warnings, et traces_rejected contient les codes de la Commission dans 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
Idempotence
Chaque POST accepte l'en-tête Idempotency-Key (jusqu'à 255 caractères). Utilisez-le toujours pour déposer : si la connexion coupe, réessayez avec la même clé sans risque de déposer deux fois.
Même clé et même requête pendant 24 heures : nous renvoyons la réponse enregistrée avec l'en-tête Idempotent-Replayed: true, sans rien répéter.
Même clé avec un autre corps ou chemin : 422 idempotency_key_reused.
Tant que la première est en cours : 409 idempotency_in_progress. Les réponses 5xx ne sont pas enregistrées : le nouvel essai est réexécuté.
Pagination et références de votre ERP
Les listes renvoient { "data": [...], "nextCursor": "..." }. Demandez la page suivante avec ?cursor=<nextCursor> ; limit va de 1 à 100 (50 par défaut). Sur la dernière page, nextCursor vaut null.
Fournisseurs, produits et lots acceptent externalRef, l'identifiant dans votre ERP (par exemple la réception WH/IN/00012). Un POST avec un externalRef existant ne crée rien : il renvoie l'existant avec 200 et duplicate: true. Recherche possible avec ?externalRef=.
GET /dds filtre par status, operatorId, batchId, internalReferenceNumber et updatedSince (pour synchroniser par interrogation si vous n'utilisez pas les webhooks).
Endpoints
Base : /api/v1. La référence complète, avec tous les champs et schémas, figure dans le document OpenAPI.
| Méthode | Chemin | Rôle |
|---|---|---|
| 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
Votre ERP enregistre son URL avec POST /webhooks (https, jamais un réseau interne) et reçoit le secret une seule fois. Chaque notification est un POST avec l'événement en JSON.
Événements
| type | |
|---|---|
dds.submitted | Une DDS a été déposée dans TRACES (depuis le web, l'API ou la synchronisation). |
dds.status_changed | Une DDS change de statut : référence reçue (AVAILABLE), rejetée, retirée ou modifiée. |
plot.proposal_answered | Le fournisseur a confirmé, corrigé ou rejeté une parcelle que vous aviez proposée. |
plot.satellite_flagged | Un contrôle par satellite a détecté une perte de couvert arboré : à examiner. |
supplier.claimed | Le fournisseur a créé son compte et revendiqué la fiche que vous aviez créée. |
plan.quota_80 | Vous avez utilisé 80 % des déclarations incluses dans votre offre. |
plan.quota_100 | Vous avez utilisé toutes les déclarations incluses ; les suivantes sont en supplément. |
POST /webhooks/{id}/test envoie un événement ping signé pour tester la connexion.
Corps de chaque notification : { "id", "type", "createdAt", "data" }. data contient l'objet sous la même forme que dans l'API (dds, plot, supplier...). En-têtes : X-Sourevo-Event, X-Sourevo-Delivery (identifiant de livraison) et 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 }
}
}Signature : X-Sourevo-Signature: t=<unix>,v1=<hex>, où v1 est le HMAC-SHA256 avec votre secret de "<t>.<corps brut>". Refusez les notifications décalées de plus de 5 minutes.
Toute réponse 2xx confirme la livraison. Sinon nous réessayons après 1 min, 5 min, 30 min, 2 h, 6 h, 12 h et 24 h ; ensuite la livraison passe en failed. Sans redirection et avec un délai de 10 secondes.
GET /webhooks/{id}/deliveries renvoie le journal des livraisons avec tentatives, dernier code HTTP et erreur.
Vérifier la signature
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", ""))Exemples curl
Remplacez svk_... par votre clé. Les identifiants proviennent des réponses précédentes.
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"]}'Créez un compte : l'essai de 14 jours inclut l'API en mode simulé.
Créer un compte gratuit