Sourevo
Développeurs

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 :

  1. Produits : créez chaque article RDUE avec son code NC (et le nom scientifique pour le bois).
  2. Fournisseurs : synchronisez vos contacts avec leur externalRef et invitez-les sur leur portail ; suivez leur niveau de confiance.
  3. Parcelles : le fournisseur les dépose sur son portail, ou vous envoyez celles qu'il vous a transmises comme proposition à confirmer.
  4. Lot : chaque réception de marchandise de l'ERP est un lot avec sa quantité et ses parcelles d'origine.
  5. DDS : créez le brouillon à partir du lot et lancez le contrôle préalable (blocages et avertissements).
  6. Dépôt : acceptez les avertissements et déposez dans TRACES.
  7. 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 18

Les 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..." }]
    }
  }
}
HTTPcodeSignification
400validation_errorRequête mal formée ou champ invalide (voir details).
400invalid_webhook_urlL'URL du webhook n'est pas en https ou pointe vers un réseau interne.
401missing_api_keyL'en-tête Authorization avec une clé est absent.
401invalid_api_keyLa clé n'existe pas ou a été révoquée.
403plan_not_allowedL'offre n'inclut pas l'API (Packs), ou l'essai ou l'offre est terminé.
403key_owner_forbiddenLa personne qui a créé la clé a quitté l'organisation ou n'est plus propriétaire ni administratrice.
403forbiddenLe rôle de la clé ne permet pas cette action, ou la ressource appartient à une autre organisation.
404not_foundN'existe pas dans votre organisation.
409conflictL'état de la ressource ne le permet pas (voir details.reason).
409idempotency_in_progressUne autre requête avec la même Idempotency-Key est encore en cours.
413payload_too_largeLe corps dépasse 2 Mo.
422idempotency_key_reusedCette Idempotency-Key a déjà servi pour une autre requête.
422unprocessableUne règle métier l'empêche (voir details.reason).
429rate_limitedPlus de 120 requêtes par minute avec cette clé.
500internal_errorErreur inattendue de notre côté ; elle est enregistrée.
502upstream_errorTRACES n'a pas pu être joint. Réessayez avec la même Idempotency-Key.
503service_unavailableUn 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éthodeCheminRôle
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

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.submittedUne DDS a été déposée dans TRACES (depuis le web, l'API ou la synchronisation).
dds.status_changedUne DDS change de statut : référence reçue (AVAILABLE), rejetée, retirée ou modifiée.
plot.proposal_answeredLe fournisseur a confirmé, corrigé ou rejeté une parcelle que vous aviez proposée.
plot.satellite_flaggedUn contrôle par satellite a détecté une perte de couvert arboré : à examiner.
supplier.claimedLe fournisseur a créé son compte et revendiqué la fiche que vous aviez créée.
plan.quota_80Vous avez utilisé 80 % des déclarations incluses dans votre offre.
plan.quota_100Vous 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

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

Exemples curl

Remplacez svk_... par votre clé. Les identifiants proviennent des réponses précédentes.

Tester la connexion
curl https://sourevo.com/api/v1/me \
  -H "Authorization: Bearer svk_..." \
  -H "X-Sourevo-Client: Odoo 18"
Créer un produit avec son code 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"}'
Créer un fournisseur
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"}'
Envoyer une parcelle comme proposition
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}'
Créer un lot à partir d'une réception
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"]}'
Créer le brouillon 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."}}'
Contrôle préalable
curl https://sourevo.com/api/v1/dds/DDS_ID/check \
  -H "Authorization: Bearer svk_..." \
  -H "X-Sourevo-Client: Odoo 18"
Déposer dans 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"]}'
Enregistrer 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"]}'
Vous commencez une intégration ?

Créez un compte : l'essai de 14 jours inclut l'API en mode simulé.

Créer un compte gratuit