Sourevo
Developers

API to connect your ERP

Everything the Sourevo app does, without a screen: your ERP creates products and suppliers, uploads plots, turns each goods receipt into a batch, prepares the due diligence statement (DDS), submits it to TRACES and gets the reference number back by webhook.

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

The flow, step by step

Designed for the Odoo module, and valid for any ERP or forwarder software:

  1. Products: create each EUDR item with its CN code (and the scientific name for wood).
  2. Suppliers: sync your contacts with their externalRef and invite them to their portal; follow their trust level.
  3. Plots: the supplier uploads them in its portal, or you upload the ones it sent you as a proposal for it to confirm.
  4. Batch: each goods receipt in the ERP is a batch with its quantity and its origin plots.
  5. DDS: create the draft from the batch and run the pre-submission check (blocking issues and warnings).
  6. Submission: acknowledge the warnings and submit to TRACES.
  7. Reference: TRACES returns the reference number and the dds.status_changed webhook brings it to your ERP.

Plans and simulated mode

The API is included in the Pro, Enterprise and Forwarder plans at no extra cost. Packs do not include it: calls answer 403 plan_not_allowed.

During the 14-day trial keys work in simulated mode: statements go to the Sourevo simulated TRACES and get references starting with SIM. They never reach the Commission.

Every response carries the X-Sourevo-Mode header (live or simulated) and GET /me returns the mode and the plan. Every DDS carries simulated: true or false.

When the trial or the plan ends, keys answer 403 plan_not_allowed with details.reason trial_expired or plan_expired.

Authentication

Every request carries the header Authorization: Bearer svk_... with a key of your organization.

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

Keys are created in the app, under API and integrations, by owners and administrators only. They are shown once: we only keep their fingerprint.

A key acts as the person who created it. If they leave the organization or stop being an administrator, the key answers 403 key_owner_forbidden. The first use of each key is notified by email.

Optional: the X-Sourevo-Client header (for example "Odoo 18") says which program is calling; it shows in the first-use email.

Rate limit

Up to 120 requests per minute per key. Every response carries X-RateLimit-Limit and X-RateLimit-Remaining; above it, 429 rate_limited with Retry-After in seconds.

Errors

Every error has the same shape, in English: { "error": { "code", "message", "details" } }. Program against code and details.reason, never against the message text.

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..." }]
    }
  }
}
HTTPcodeMeaning
400validation_errorMalformed request or an invalid field (see details).
400invalid_webhook_urlThe webhook URL is not https or points to an internal network.
401missing_api_keyNo Authorization header with a key.
401invalid_api_keyThe key does not exist or has been revoked.
403plan_not_allowedThe plan does not include the API (Packs), or the trial or plan has ended.
403key_owner_forbiddenThe key creator left the organization or is no longer an owner or administrator.
403forbiddenThe role behind the key cannot do this, or the resource belongs to another organization.
404not_foundIt does not exist in your organization.
409conflictThe state of the resource does not allow it (see details.reason).
409idempotency_in_progressAnother request with the same Idempotency-Key is still running.
413payload_too_largeThe body is larger than 2 MB.
422idempotency_key_reusedThat Idempotency-Key was already used with a different request.
422unprocessableA business rule prevents it (see details.reason).
429rate_limitedMore than 120 requests per minute with this key.
500internal_errorUnexpected error on our side; it is logged.
502upstream_errorTRACES could not be reached. Retry with the same Idempotency-Key.
503service_unavailableAn optional service (satellite check) is not configured.

Reasons (details.reason)

Business rules answer 422 unprocessable (or 409 conflict, 400 validation_error) with a details.reason from this closed list, in snake_case, plus the data you need to build your own message. For example, pre_submission_blocked carries blocking and warnings, and traces_rejected carries the Commission codes 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

Idempotency

Every POST accepts the Idempotency-Key header (up to 255 characters). Always use it when submitting: if the connection drops, retry with the same key without fear of submitting twice.

Same key and same request within 24 hours: we return the stored response with the header Idempotent-Replayed: true, repeating nothing.

Same key with another body or path: 422 idempotency_key_reused.

While the first one is still running: 409 idempotency_in_progress. 5xx responses are not stored: the retry runs again.

Pagination and your ERP references

Lists return { "data": [...], "nextCursor": "..." }. Ask for the next page with ?cursor=<nextCursor>; limit goes from 1 to 100 (50 by default). On the last page nextCursor is null.

Suppliers, products and batches accept externalRef, the id in your ERP (for example the goods receipt WH/IN/00012). A POST with an externalRef that already exists creates nothing: it returns the existing one with 200 and duplicate: true. You can search with ?externalRef=.

GET /dds filters by status, operatorId, batchId, internalReferenceNumber and updatedSince (to sync by polling if you do not use webhooks).

Endpoints

Base: /api/v1. The full reference, with every field and schema, is in the OpenAPI document.

MethodPathWhat it does
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

Your ERP registers its URL with POST /webhooks (https, never an internal network) and gets the secret once. Each notice is a POST with the event as JSON.

Events

type
dds.submittedA DDS was submitted to TRACES (from the web, the API or the sync).
dds.status_changedA DDS changes status: reference received (AVAILABLE), rejected, withdrawn or amended.
plot.proposal_answeredThe supplier confirmed, corrected or rejected a plot you uploaded as a proposal.
plot.satellite_flaggedA satellite check found tree cover loss: it needs review.
supplier.claimedThe supplier created its account and claimed the record you created.
plan.quota_80You have used 80% of the statements included in your plan.
plan.quota_100You have used every included statement; the next ones are extra.

POST /webhooks/{id}/test sends a signed ping event to test the connection.

Body of each notice: { "id", "type", "createdAt", "data" }. data carries the object in the same shape as in the API (dds, plot, supplier...). Headers: X-Sourevo-Event, X-Sourevo-Delivery (delivery id) and 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>, where v1 is HMAC-SHA256 with your secret of "<t>.<raw body>". Refuse notices more than 5 minutes off.

Any 2xx response confirms the delivery. Otherwise we retry after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h; then the delivery is failed. No redirects and a 10-second timeout.

GET /webhooks/{id}/deliveries returns the delivery log with attempts, last HTTP status and error.

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

curl examples

Replace svk_... with your key. Ids come from the previous responses.

Test the connection
curl https://sourevo.com/api/v1/me \
  -H "Authorization: Bearer svk_..." \
  -H "X-Sourevo-Client: Odoo 18"
Create a product with its CN code
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"}'
Create a supplier
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"}'
Upload a plot as a proposal
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}'
Create a batch from a goods receipt
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"]}'
Create the DDS draft
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."}}'
Pre-submission check
curl https://sourevo.com/api/v1/dds/DDS_ID/check \
  -H "Authorization: Bearer svk_..." \
  -H "X-Sourevo-Client: Odoo 18"
Submit to 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"]}'
Register a 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"]}'
Starting an integration?

Create an account: the 14-day trial includes the API in simulated mode.

Create a free account