Sourevo
Desarrolladores

API para conectar tu ERP

Todo lo que hace la aplicación de Sourevo, sin pantalla: tu ERP da de alta productos y proveedores, sube parcelas, convierte cada recepción de mercancía en un lote, prepara la declaración (DDS), la presenta en TRACES y recibe el número de referencia por webhook.

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

El flujo, paso a paso

Pensado para el módulo de Odoo, vale para cualquier ERP o programa de un transitario:

  1. Productos: da de alta cada artículo EUDR con su código NC (y el nombre científico si es madera).
  2. Proveedores: sincroniza tus contactos con su externalRef e invítalos a su portal; sigue su nivel de confianza.
  3. Parcelas: el proveedor las sube en su portal, o tú subes las que te mandó como propuesta para que las confirme.
  4. Lote: cada recepción de mercancía del ERP es un lote con su cantidad y sus parcelas de origen.
  5. DDS: crea el borrador desde el lote y lanza la comprobación previa (bloqueos y avisos).
  6. Presentación: acepta los avisos y presenta en TRACES.
  7. Referencia: TRACES devuelve el número de referencia y el webhook dds.status_changed lo lleva a tu ERP.

Planes y modo simulado

La API está incluida en los planes Pro, Empresa y Transitario, sin coste extra. Los Packs no la incluyen: la llamada responde 403 plan_not_allowed.

Durante la prueba de 14 días las claves funcionan en modo simulado: las DDS van al TRACES simulado de Sourevo y llevan referencias que empiezan por SIM. Nunca llegan a la Comisión.

Cada respuesta lleva la cabecera X-Sourevo-Mode (live o simulated) y GET /me devuelve el modo y el plan. Cada DDS lleva simulated: true o false.

Si la prueba o el plan terminan, las claves responden 403 plan_not_allowed con details.reason trial_expired o plan_expired.

Autenticación

Cada petición lleva la cabecera Authorization: Bearer svk_... con una clave de tu organización.

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

Las claves se crean en la aplicación, en API e integraciones, solo por propietarios y administradores. Se enseñan una sola vez: guardamos solo su huella.

La clave actúa como la persona que la creó. Si deja la organización o deja de ser administradora, la clave responde 403 key_owner_forbidden. El primer uso de cada clave se avisa por correo.

Opcional: la cabecera X-Sourevo-Client (por ejemplo "Odoo 18") dice qué programa llama; sale en el aviso del primer uso.

Límite de peticiones

Hasta 120 peticiones por minuto y clave. Cada respuesta lleva X-RateLimit-Limit y X-RateLimit-Remaining; al pasarte, 429 rate_limited con Retry-After en segundos.

Errores

Todos los errores tienen la misma forma, en inglés: { "error": { "code", "message", "details" } }. Programa contra code y details.reason, nunca contra el texto 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..." }]
    }
  }
}
HTTPcodeQué significa
400validation_errorPetición mal formada o un campo no válido (ver details).
400invalid_webhook_urlLa URL del webhook no es https o apunta a una red interna.
401missing_api_keyFalta la cabecera Authorization con una clave.
401invalid_api_keyLa clave no existe o está revocada.
403plan_not_allowedEl plan no incluye la API (Packs) o la prueba o el plan han terminado.
403key_owner_forbiddenQuien creó la clave ya no está en la organización o ya no es propietario ni administrador.
403forbiddenEl rol de la clave no permite esto, o el recurso es de otra organización.
404not_foundNo existe en tu organización.
409conflictEl estado del recurso no lo permite (ver details.reason).
409idempotency_in_progressOtra petición con la misma Idempotency-Key sigue en curso.
413payload_too_largeEl cuerpo pasa de 2 MB.
422idempotency_key_reusedEsa Idempotency-Key ya se usó con otra petición.
422unprocessableUna regla de negocio lo impide (ver details.reason).
429rate_limitedMás de 120 peticiones por minuto con esta clave.
500internal_errorError inesperado de nuestro lado; queda registrado.
502upstream_errorNo se ha podido contactar con TRACES. Reintenta con la misma Idempotency-Key.
503service_unavailableUn servicio opcional (comprobación por satélite) no está configurado.

Motivos (details.reason)

Las reglas de negocio responden 422 unprocessable (o 409 conflict, 400 validation_error) con un details.reason de esta lista cerrada, en snake_case, y los datos que necesites para montar tu propio mensaje. Por ejemplo, pre_submission_blocked trae blocking y warnings, y traces_rejected trae los códigos de la Comisión en 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

Idempotencia

Todo POST acepta la cabecera Idempotency-Key (hasta 255 caracteres). Úsala siempre al presentar: si se corta la conexión, reintenta con la misma clave sin miedo a presentar dos veces.

Misma clave y misma petición durante 24 horas: devolvemos la respuesta guardada con la cabecera Idempotent-Replayed: true, sin repetir nada.

Misma clave con otro cuerpo o ruta: 422 idempotency_key_reused.

Si la primera sigue en curso: 409 idempotency_in_progress. Las respuestas 5xx no se guardan: el reintento se ejecuta de nuevo.

Paginación y referencias de tu ERP

Las listas devuelven { "data": [...], "nextCursor": "..." }. Pide la siguiente página con ?cursor=<nextCursor>; limit va de 1 a 100 (50 por defecto). En la última página nextCursor es null.

Proveedores, productos y lotes aceptan externalRef, el id en tu ERP (por ejemplo el albarán de entrada WH/IN/00012). Un POST con un externalRef que ya existe no crea otro: devuelve el existente con 200 y duplicate: true. Puedes buscar por ?externalRef=.

GET /dds filtra por status, operatorId, batchId, internalReferenceNumber y updatedSince (para sincronizar por sondeo si no usas webhooks).

Endpoints

Base: /api/v1. La referencia completa, con todos los campos y esquemas, está en el documento OpenAPI.

MétodoRutaQué hace
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

Tu ERP da de alta su URL con POST /webhooks (https, nunca una red interna) y recibe el secreto una sola vez. Cada aviso es un POST con el evento en JSON.

Eventos

type
dds.submittedUna DDS se ha presentado en TRACES (desde la web, la API o el sincronizador).
dds.status_changedCambia el estado de una DDS: referencia recibida (AVAILABLE), rechazada, retirada o modificada.
plot.proposal_answeredEl proveedor ha confirmado, corregido o rechazado una parcela que subiste como propuesta.
plot.satellite_flaggedUna comprobación por satélite ha detectado pérdida de cubierta arbórea: hay que revisarla.
supplier.claimedEl proveedor ha creado su cuenta y ha reclamado la ficha que diste de alta.
plan.quota_80Has usado el 80 % de las declaraciones incluidas en tu plan.
plan.quota_100Has usado todas las declaraciones incluidas; las siguientes son extra.

POST /webhooks/{id}/test manda un evento ping firmado para probar la conexión.

Cuerpo de cada aviso: { "id", "type", "createdAt", "data" }. data lleva el objeto con la misma forma que en la API (dds, plot, supplier...). Cabeceras: X-Sourevo-Event, X-Sourevo-Delivery (id de la entrega) y 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 }
  }
}

Firma: X-Sourevo-Signature: t=<unix>,v1=<hex>, donde v1 es HMAC-SHA256 con tu secreto de "<t>.<cuerpo tal cual>". Rechaza avisos con más de 5 minutos de diferencia.

Cualquier respuesta 2xx confirma la entrega. Si no, reintentamos tras 1 min, 5 min, 30 min, 2 h, 6 h, 12 h y 24 h; después la entrega queda como failed. Sin redirecciones y con 10 segundos de espera.

GET /webhooks/{id}/deliveries devuelve el registro de entregas con intentos, último código HTTP y error.

Verificar la firma

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

Ejemplos con curl

Sustituye svk_... por tu clave. Los ids vienen de las respuestas anteriores.

Comprobar la conexión
curl https://sourevo.com/api/v1/me \
  -H "Authorization: Bearer svk_..." \
  -H "X-Sourevo-Client: Odoo 18"
Crear un producto con su código 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"}'
Crear un proveedor
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"}'
Subir una parcela como propuesta
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}'
Crear un lote desde una recepción
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"]}'
Crear el borrador 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."}}'
Comprobación previa
curl https://sourevo.com/api/v1/dds/DDS_ID/check \
  -H "Authorization: Bearer svk_..." \
  -H "X-Sourevo-Client: Odoo 18"
Presentar en 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"]}'
Dar de alta 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"]}'
¿Empiezas a integrar?

Crea una cuenta: la prueba de 14 días incluye la API en modo simulado.

Crear cuenta gratis