API Documentation

Sourced Public API v1

The Sourced API enables ERP systems (such as Calipso, SAP, JD Edwards) to integrate directly with Sourced's procurement platform. Use it to:

  • Push purchase requisitions from your ERP into Sourced
  • Pull purchase orders back into your ERP once awarded
  • Check which requisitions have already been synced
  • Monitor the status of your requisitions

The API follows REST conventions, uses JSON for request and response bodies, and authenticates via API keys.

Authentication

All endpoints (except health check) require an API key passed in the X-API-Key header.

API keys are created in the Sourced admin panel under Settings → API Keys. Each key is shown only once upon creation. Store it securely.

Example: Authenticated request
curl -H "X-API-Key: sk_live_abc123..." \
  https://api.gosourced.ai/api/v1/requisitions
Security: Treat your API key like a password. Do not expose it in client-side code, public repositories, or logs. If compromised, revoke it immediately from the admin panel and create a new one.

Base URL

EnvironmentBase URL
Productionhttps://api.gosourced.ai
Developmenthttps://api-dev.gosourced.ai

All endpoints are prefixed with /api/v1.

Rate Limiting

All endpoints are limited to 60 requests per minute per endpoint. Exceeding this limit returns a 429 Too Many Requests response.

For bulk operations, use the batch import endpoint (POST /requisitions) which accepts up to 100 requisitions per request.

Errors

The API uses standard HTTP status codes. Errors return a JSON body with a detail field.

Error response format
{
  "detail": "Invalid or revoked API key."
}
StatusMeaning
200Success
400Bad Request - validation error or malformed body
401Unauthorized - missing or invalid API key
404Not Found - resource does not exist or is not accessible
422Unprocessable Entity - request body failed schema validation
429Too Many Requests - rate limit exceeded
500Internal Server Error - unexpected error on our side

Error Examples

401 - Invalid API key
{
  "detail": "Invalid or revoked API key."
}
422 - Validation error (e.g., missing required field)
{
  "detail": [
    {
      "loc": ["body", "requisitions", 0, "items", 0, "quantity"],
      "msg": "Input should be greater than 0",
      "type": "greater_than"
    }
  ]
}
200 - Partial success (some requisitions failed)
{
  "success": false,
  "created_count": 2,
  "updated_count": 0,
  "skipped_count": 0,
  "error_count": 1,
  "errors": [
    {
      "index": 2,
      "external_id": "REQ-2026-0099",
      "error": "Failed to process requisition: duplicate external_line_id within items"
    }
  ],
  "requisition_ids": [1234, 1235]
}

Note: the POST /requisitions endpoint returns 200 even with partial failures. Always check the success field and the errors array to detect per-requisition issues.

Health Check

GET/api/v1/health

Verify the API is reachable. No authentication required.

Request
curl https://api.gosourced.ai/api/v1/health
Response - 200 OK
{
  "status": "ok",
  "api": "v1"
}

Upload a File (Presigned)

POST/api/v1/files/presignAPI Key Required

Get a presigned URL to upload a single file (a requisition manifest or an attachment) directly to S3. The bytes never pass through the API, so large tender specifications upload without hitting payload limits.

Transport channel only - files land in object storage under your organization's prefix, with no parsing or processing. The presigned URL expires in 10 minutes and accepts files up to 500 MB.

Two-step flow (per file)

  1. Call this endpoint with the filename to receive an upload_url and the form fields.
  2. POST the file as multipart/form-data to upload_url, including every entry in fields first, then a file field with the bytes.

Request Body

FieldTypeRequiredDescription
filenamestringrequiredOriginal filename (e.g. 'SOL_106177_LINE_001_SEQ010_adjunto.docx'). Path components are stripped.
content_typestringoptionalMIME type. If omitted, any content type is accepted.
1. Request
curl -X POST https://api.gosourced.ai/api/v1/files/presign \
  -H "X-API-Key: sk_live_abc123..." \
  -H "Content-Type: application/json" \
  -d '{
    "filename": "SOL_106177_LINE_001_SEQ010_adjunto.docx",
    "content_type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
  }'
Response - 200 OK
{
  "upload_url": "https://s3.amazonaws.com/your-bucket",
  "fields": {
    "key": "erp-inbound/42/SOL_106177_LINE_001_SEQ010_adjunto.docx",
    "Content-Type": "application/vnd.openxmlformats-...",
    "policy": "eyJleHBpcmF0aW9uIjoi...",
    "x-amz-signature": "a1b2c3..."
  },
  "key": "erp-inbound/42/SOL_106177_LINE_001_SEQ010_adjunto.docx",
  "max_file_size": 524288000,
  "expires_in": 600
}

Response fields

  • upload_url - the S3 URL to POST the file to.
  • fields - form fields that must be included in the multipart POST, before the file field.
  • key - the object key the file will be stored under.
  • max_file_size - maximum allowed size in bytes (enforced by S3).
  • expires_in - seconds until the presigned URL expires.
2. Upload to S3 (multipart/form-data)
# Use upload_url + every key in "fields", then the file (last).
curl -X POST "$UPLOAD_URL" \
  -F "key=erp-inbound/42/SOL_106177_LINE_001_SEQ010_adjunto.docx" \
  -F "Content-Type=application/vnd.openxmlformats-..." \
  -F "policy=eyJleHBpcmF0aW9uIjoi..." \
  -F "x-amz-signature=a1b2c3..." \
  -F "file=@SOL_106177_LINE_001_SEQ010_adjunto.docx"
# HTTP 204 No Content on success

Import Requisitions

POST/api/v1/requisitionsAPI Key Required

Import one or more purchase requisitions from your ERP. Supports batch import of up to 100 requisitions per request. Uses external_id for deduplication.

Request Body

The body must contain a requisitions array with 1 to 100 requisition objects.

Requisition Object

FieldTypeRequiredDescription
external_idstringrequiredUnique ID in your ERP (deduplication key). Max 250 chars.
itemsarrayrequiredLine items (1-200 items). See Item schema below.
requester_namestringoptionalName of the person requesting. Max 200 chars.
requester_emailstringoptionalEmail of the requester. Max 200 chars.
assigned_buyerstringoptionalBuyer assigned in the source ERP (free text). Shown and filterable in triage. Max 200 chars.
descriptionstringoptionalRequisition title or summary. Max 500 chars.
commentsstringoptionalAdditional instructions for buyers (HTML supported). Max 15,000 chars.
delivery_addressstringoptionalFree-text delivery address. Max 500 chars.
delivery_address_codestringoptionalCode matching an address configured in Sourced. Max 50 chars.
desired_delivery_lead_time_daysintegeroptionalDesired lead time in days from PO confirmation. Applied as default to all items.
created_datedatetimeoptionalWhen the requisition was created in your system (ISO 8601). Shown as "Created" in triage. Not a delivery date: the need-by date goes per item in desired_delivery_date.
offer_deadlinedatetimeoptionalDeadline for supplier quotations (ISO 8601).
total_estimated_valuefloatoptionalTotal estimated value. Auto-calculated from items if omitted.
currencystringoptionalCurrency code (e.g., "ARS", "USD", "PYG"). Defaults to your organization's local currency. Max 10 chars.
department_codestringoptionalDepartment/cost center code (matched against Sourced departments). Max 50 chars.
priority_levelstringoptionalPriority: "LOW", "MEDIUM", "HIGH", or "CRITICAL".
attachmentsarrayoptionalFile attachments (max 20). See Attachment schema below.
raw_dataobjectoptionalArbitrary JSON from your ERP, stored for traceability.

Item Object

FieldTypeRequiredDescription
descriptionstringrequiredShared with supplierItem name or description. Max 1,000 chars.
quantityfloatrequiredShared with supplierRequired quantity (must be > 0).
external_line_idstringoptionalLine ID in your ERP (e.g., "REQ-001-L10"). Returned in POs for traceability. Max 100 chars.
unit_of_measurestringoptionalShared with supplierUOM code (e.g., "KG", "EA", "LT", "M", "UN"). Matched against your org catalog. Max 50 chars.
target_pricefloatoptionalTarget/budget price per unit.
estimated_pricefloatoptionalEstimated total price for this line.
currencystringoptionalCurrency for prices (e.g., "ARS", "USD"). Max 10 chars.
categorystringoptionalCategory from your ERP. Max 200 chars.
material_codestringoptionalShared with supplierMaterial/part code in your ERP (e.g., SAP material code). Max 100 chars.
specificationsobjectoptionalTechnical specifications. See Specifications schema below.
desired_delivery_datedatetimeoptionalDesired delivery date for this item (ISO 8601, e.g., "2026-05-15T00:00:00Z").
desired_delivery_lead_time_daysintegeroptionalDesired lead time in days for this line item. Overrides the header-level value.
detailstringoptionalShared with supplierTechnical detail or note for this line. Max 1,000 chars. Shared with the supplier: included in the quotation request email under “Specifications”, unless you send specifications.technical_specs, which takes precedence. Do not use it for internal buyer notes.
raw_dataobjectoptionalArbitrary JSON for this line item.

Specifications Object

FieldTypeRequiredDescription
manufacturer_codestringoptionalShared with supplierManufacturer part number (e.g., "6ES7214-1AG40-0XB0"). Max 100 chars.
manufacturer_namestringoptionalShared with supplierManufacturer name (e.g., "Siemens"). Max 200 chars.
manufacturer_descriptionstringoptionalManufacturer's description. Max 500 chars.
buyer_codestringoptionalShared with supplierInternal code in your system (e.g., "MAT-001234"). Max 100 chars.
buyer_code_descriptionstringoptionalDescription for the internal code. Max 500 chars.
buyer_code_systemstringoptionalSource system name (e.g., "SAP", "Calipso"). Max 50 chars.
technical_specsstringoptionalShared with supplierFree-text technical specs (e.g., "220V, 50Hz, IP55"). Max 2,000 chars. Shared with the supplier in the quotation request email. Takes precedence over the item's detail.
requirementsstringoptionalAdditional requirements (e.g., "ISO 9001 certification required"). Max 2,000 chars. Internal: not included in the email to the supplier.

Attachment Object

FieldTypeRequiredDescription
urlstringrequiredPublicly accessible URL to download the file. Max 2,000 chars.
filenamestringrequiredOriginal filename (e.g., "plano_motor.pdf"). Max 255 chars.
descriptionstringoptionalDescription of the attachment. Max 500 chars.
file_typestringoptionalMIME type (auto-detected if not provided). Max 100 chars.

Deduplication Logic

Each requisition is identified by its external_id (scoped to your organization):

  • New: If no existing requisition is found, a new one is created with status PENDING.
  • Update: If an existing requisition with status PENDING is found, it is updated in-place (items are fully replaced).
  • Supersede: If an existing requisition with status SUPERSEDED is found, it is reactivated (back to PENDING) with the new payload.
  • Launched: If the requisition was already launched as a Purchase Request (LAUNCHED), the import is rejected. The active PR cannot be overwritten.
  • Discarded: If the requisition was previously discarded (DISCARDED), it is reactivated (back to PENDING) with the new payload. Useful when the ERP cancels and later re-sends it.
Request - Minimal example
curl -X POST https://api.gosourced.ai/api/v1/requisitions \
  -H "X-API-Key: sk_live_abc123..." \
  -H "Content-Type: application/json" \
  -d '{
    "requisitions": [
      {
        "external_id": "REQ-2026-0042",
        "items": [
          {
            "description": "Bearing SKF 6205",
            "quantity": 10
          }
        ]
      }
    ]
  }'

Response

ERPImportResult

FieldTypeRequiredDescription
successbooleanrequiredtrue if error_count is 0.
created_countintegerrequiredNumber of new requisitions created.
updated_countintegerrequiredNumber of existing PENDING requisitions updated.
skipped_countintegerrequiredNumber of skipped requisitions (currently always 0).
error_countintegerrequiredNumber of requisitions that failed to import.
errorsarrayrequiredArray of {index, external_id, error} for each failed requisition.
requisition_idsarrayrequiredInternal Sourced IDs of created/updated requisitions.
Response - 200 OK
{
  "success": true,
  "created_count": 1,
  "updated_count": 0,
  "skipped_count": 0,
  "error_count": 0,
  "errors": [],
  "requisition_ids": [1234]
}

List Requisitions

GET/api/v1/requisitionsAPI Key Required

Retrieve a paginated list of your imported requisitions. Returns only requisitions created via the API.

Query Parameters

FieldTypeRequiredDescription
pageintegeroptionalPage number (default: 1).
page_sizeintegeroptionalItems per page, 1-100 (default: 20).
statusstringoptionalFilter by status: "PENDING", "LAUNCHED", "DISCARDED", "SUPERSEDED".
Request
curl -H "X-API-Key: sk_live_abc123..." \
  "https://api.gosourced.ai/api/v1/requisitions?page=1&page_size=20&status=PENDING"
Response - 200 OK
{
  "data": [
    {
      "id": 1234,
      "external_id": "REQ-2026-0042",
      "status": "PENDING",
      "requester_name": "Juan Pérez",
      "description": "Repuestos línea producción",
      "items": [...],
      "imported_at": "2026-03-07T14:30:00Z"
    }
  ],
  "total": 45,
  "page": 1,
  "page_size": 20,
  "has_more": true
}

Get Requisition Detail

GET/api/v1/requisitions/{requisition_id}API Key Required

Retrieve a single requisition by its internal Sourced ID, including all items.

Request
curl -H "X-API-Key: sk_live_abc123..." \
  https://api.gosourced.ai/api/v1/requisitions/1234

Returns the full requisition object. Returns 404 if not found or not owned by your organization.

Check Existing Requisitions

POST/api/v1/requisitions/check-existingAPI Key Required

Check which external_ids already exist in Sourced before importing. Useful to avoid unnecessary API calls.

Request Body

FieldTypeRequiredDescription
external_idsarrayrequiredList of external_id strings to check (1-100 items).
Request
curl -X POST https://api.gosourced.ai/api/v1/requisitions/check-existing \
  -H "X-API-Key: sk_live_abc123..." \
  -H "Content-Type: application/json" \
  -d '{
    "external_ids": ["REQ-2026-0042", "REQ-2026-0043", "REQ-2026-0044"]
  }'
Response - 200 OK
{
  "existing": ["REQ-2026-0042"],
  "not_found": ["REQ-2026-0043", "REQ-2026-0044"]
}

Cancel Requisition

POST/api/v1/requisitions/cancelAPI Key Required

Cancel a requisition by its external_id: PENDING (discarded) or LAUNCHED (the Purchase Request is cancelled in Sourced).

A PENDING requisition is discarded. A LAUNCHED one (already converted into a Purchase Request) can also be cancelled from here: if no line has a purchase order, the whole Purchase Request is cancelled; if some line already has a PO, see the rules below. Sourced never cancels a purchase order in cascade. A PENDING requisition with some lines already launched to a Purchase Request follows the same rules as a LAUNCHED one for that request, and its pending lines are discarded in the same operation; a 409 modifies nothing (not even the pending lines).

FieldTypeRequiredDescription
external_idstringrequiredThe requisition ID in your external system (the same one used when importing)
reasonstringoptionalCancellation reason (stored for audit purposes)
cancel_remainingbooleanoptionalOnly for requisitions with lines launched to a Purchase Request (LAUNCHED, or PENDING partially launched) and some line already awarded: instead of responding 409, close the balance (cancel the lines without a PO, pending lines included, and leave the POs untouched).

LAUNCHED requisition (or PENDING with lines already launched)

  • No purchase orders: the Purchase Request becomes CANCELLED, pending quotations are rejected and the requisition becomes DISCARDED with the reason sent. Responds cancelled. Same as the "Cancel" button on the request in Sourced.
  • Some line awarded (without cancel_remaining): responds 409 requisitionPartiallyAwarded with awarded_lines: each purchased line with its po_code and po_status. Nothing is modified.
  • With cancel_remaining=true: balance close: only the lines without a PO are cancelled, the Purchase Request becomes CLOSED and the POs are untouched. In the requisition, purchased lines stay LAUNCHED and the rest become DISCARDED. Responds partially_cancelled with the POs still alive. Same as "Close balance" in Sourced. Idempotent: if there is no balance left (a retry after the balance was already closed, or every line already has a PO), it responds partially_cancelled again, with the same data, without modifying anything or sending the webhook again.
  • Cancel it within Sourced: if the Purchase Request is shared with other requisitions (or the requisition is split across several), responds 409 requisitionSharedPrCancelInApp; if some line was launched to a tender or the Purchase Request has a linked tender that is not cancelled, 409 requisitionTenderCancelInApp. Nothing is modified.
  • To void the purchases too: first cancel each PO with POST /purchase-orders/{po_id}/status (status CANCELLED) and then call this endpoint: with no active POs, the whole Purchase Request is cancelled. Each step is explicit about what it voids.
Request
curl -X POST https://api.gosourced.ai/api/v1/requisitions/cancel \
  -H "X-API-Key: sk_live_abc123..." \
  -H "Content-Type: application/json" \
  -d '{
    "external_id": "REQ-2026-0042",
    "reason": "Cancelado desde ERP"
  }'
Response - 200 OK
// PENDING
{ "status": "discarded", "external_id": "REQ-2026-0042" }

// LAUNCHED sin OC: se cancela la Solicitud de Compra
{ "status": "cancelled", "external_id": "REQ-2026-0042", "pr_id": 8123, "pr_code": "PR-2026-0311" }

// LAUNCHED con OC y cancel_remaining=true: cierre de saldo
{ "status": "partially_cancelled", "external_id": "REQ-2026-0042", "pr_id": 8123,
  "pr_code": "PR-2026-0311", "cancelled_lines": 2, "purchase_orders": ["OC-2026-0455"] }

// LAUNCHED con OC sin cancel_remaining
// 409 Conflict
{
  "detail": {
    "code": "requisitionPartiallyAwarded",
    "message": "Requisition 'REQ-2026-0042' (PR PR-2026-0311) already has purchase orders for some lines. ...",
    "params": { "external_id": "REQ-2026-0042", "pr_code": "PR-2026-0311" },
    "awarded_lines": [
      { "external_line_id": "10", "description": "Rodamiento SKF 6205",
        "po_code": "OC-2026-0455", "po_status": "SENT" }
    ]
  }
}

Possible responses:

  • discarded - Requisition was successfully discarded.
  • cancelled - Requisition launched (entirely or partially) without POs: the Purchase Request was cancelled and the requisition discarded.
  • partially_cancelled - Requisition with POs and cancel_remaining=true: the balance is closed. cancelled_lines counts the Purchase Request lines left cancelled (without a PO) and purchase_orders lists the POs still alive. A retry returns the same response.
  • already_discarded - Requisition was already discarded (idempotent).
  • already_cancelled - The Purchase Request was already cancelled within Sourced: the requisition is discarded anyway (idempotent). If the Purchase Request had live POs, its purchased lines are NOT discarded (they stay launched) and purchase_orders lists those POs.
  • already_superseded - Requisition was already superseded by a newer version.
  • LAUNCHED requisition with some PO and no cancel_remaining - returns 409 requisitionPartiallyAwarded with awarded_lines.
  • The Purchase Request is being processed right now (for example, an award in progress) - returns 409 requisitionPrBusy with a Retry-After header. Nothing was changed: it is retryable, call again with the same data in a few seconds.
  • The requisition itself is being processed right now (for example, being launched from triage or updated by another import) - returns 409 requisitionBusy with a Retry-After header. Nothing was changed: it is retryable, call again with the same data in a few seconds.
  • Requisition not found - returns 404.

List Purchase Orders

GET/api/v1/purchase-ordersAPI Key Required

Retrieve purchase orders for your organization. Supports incremental sync via the 'since' parameter.

Query Parameters

FieldTypeRequiredDescription
pageintegeroptionalPage number (default: 1).
page_sizeintegeroptionalItems per page, 1-100 (default: 20).
statusstringoptionalFilter by status: "DRAFT", "CREATED", "SENT", "CONFIRMED", "REJECTED", "CANCELLED".
sincedatetimeoptionalISO 8601 timestamp. Returns only POs created or updated after this date.
requisition_external_idstringoptionalFilter by the original requisition's external_id from your ERP. Returns POs generated from that requisition.
Request - Incremental sync
curl -H "X-API-Key: sk_live_abc123..." \
  "https://api.gosourced.ai/api/v1/purchase-orders?since=2026-03-01T00:00:00Z&status=CONFIRMED"
Response - 200 OK
{
  "data": [
    {
      "id": 567,
      "code": "PO-2026-0089",
      "status": "CONFIRMED",
      "award_type": "FULL",
      "supplier_name": "Distribuidora Industrial SA",
      "supplier_id": 42,
      "supplier_external_id": "PROV-CAL-00123",
      "supplier_tax_id": "30712345678",
      "supplier_email": "ventas@distribuidora.com",
      "total_price": 285000.00,
      "currency": "ARS",
      "delivery_address": "Av. Corrientes 1234, CABA",
      "delivery_address_code": "001",
      "observations": "Entregar en horario de mañana. Coordinar con depósito.",
      "delivery_lead_time_days": 15,
      "expected_delivery_date": "2026-03-20T16:00:00Z",
      "payment_terms_code": "NET30",
      "awarded_at": "2026-03-05T16:00:00Z",
      "awarded_by_name": "Juan Pérez",
      "awarded_by_email": "juan.perez@empresa.com",
      "created_at": "2026-03-05T16:00:00Z",
      "updated_at": "2026-03-06T10:30:00Z",
      "purchase_request_id": 1234,
      "requisition_external_id": "REQ-2026-00142",
      "exchange_rate_data": {
        "date": "2026-03-05",
        "usdToArs": 1450.0,
        "arsToUsd": 0.00069
      },
      "total_nominal_savings": 15000.00,
      "total_real_savings": 8500.00,
      "savings_currency": "ARS",
      "items": [
        {
          "description": "Bearing SKF 6205",
          "quantity": 10,
          "unit_price": 28500.00,
          "total_price": 285000.00,
          "unit_of_measure": "UN",
          "currency": "ARS",
          "delivery_lead_time_days": 15,
          "expected_delivery_date": "2026-03-20T16:00:00Z",
          "external_line_id": "REQ-2026-0042-L10",
          "buyer_code": "MAT-001234",
          "buyer_code_system": "SAP",
          "manufacturer_code": "6205-2RS",
          "technical_specs": "220V, 50Hz, IP55"
        }
      ]
    }
  ],
  "total": 12,
  "page": 1,
  "page_size": 20,
  "has_more": false
}

Get Purchase Order Detail

GET/api/v1/purchase-orders/{po_id}API Key Required

Retrieve a single purchase order by its internal Sourced ID, including all line items with traceability back to your ERP via external_line_id.

Request
curl -H "X-API-Key: sk_live_abc123..." \
  https://api.gosourced.ai/api/v1/purchase-orders/567
Looking up POs by your requisition ID?

This endpoint requires the internal Sourced ID (po_id). If you need to find purchase orders using your ERP's requisition ID, use the GET /purchase-orders endpoint with the requisition_external_id query parameter instead:

GET /api/v1/purchase-orders?requisition_external_id=YOUR-REQ-ID

You can also combine it with other filters: status to filter by PO status, since for incremental sync, and page / page_size for pagination.

Purchase Order Object

FieldTypeRequiredDescription
idintegerrequiredSourced internal PO ID.
codestringrequiredHuman-readable PO code.
statusstringrequiredPO status: "DRAFT", "CREATED", "SENT", "CONFIRMED", "REJECTED", "CANCELLED".
award_typestringoptionalAward type: FULL (single supplier) or PARTIAL (split award across multiple suppliers).
supplier_namestringrequiredSupplier name.
supplier_idintegeroptionalSourced internal supplier ID.
total_pricefloatoptionalTotal PO value.
currencystringoptionalCurrency code.
delivery_addressstringoptionalDelivery address.
delivery_address_codestringoptionalCode of the linked organization delivery address (e.g. "001"). Use it to map the destination back to your ERP. Null for free-text addresses with no linked record.
observationsstringoptionalFree-text comments / observations entered by the buyer in the pre-award review step. Falls back to special conditions quoted by the supplier when the buyer left no note. Null when neither is present.
delivery_lead_time_daysintegeroptionalHeader-level delivery lead time in days, as quoted by the supplier. When the supplier quotes different lead times per line, items can have different values - see items[].delivery_lead_time_days for the per-line value.
expected_delivery_datedatetimeoptionalComputed delivery date (awarded_at + delivery_lead_time_days). Null if either input is missing. Use items[].expected_delivery_date for per-line dates.
payment_terms_codestringoptionalPayment terms code.
awarded_atdatetimeoptionalWhen the PO was awarded (ISO 8601).
awarded_by_namestringoptionalName of the user who awarded this PO.
awarded_by_emailstringoptionalEmail of the user who awarded this PO.
created_atdatetimeoptionalCreation timestamp (ISO 8601).
updated_atdatetimeoptionalLast update timestamp (ISO 8601).
purchase_request_idintegeroptionalID of the originating purchase request.
requisition_external_idstringoptionalSource-system document ID from your ERP (e.g. requisition number in Calipso/SAP/JDE). Resolved from the originating ERPRequisition when available; otherwise falls back to the PR's external_id. May be a comma-separated list when a single PR aggregates multiple source requisitions.
exchange_rate_dataobjectoptionalExchange rates at award time. Format: {"date": "2026-03-05", "usdToArs": 1450.0, "arsToUsd": 0.00069}. Null if no data available.
total_nominal_savingsfloatoptionalTotal nominal savings vs historical prices (no inflation adjustment).
total_real_savingsfloatoptionalTotal real savings vs historical prices (inflation adjusted).
savings_currencystringoptionalCurrency of the savings amounts.
itemsarrayrequiredPO line items. See PO Item schema below.

PO Item Object

FieldTypeRequiredDescription
descriptionstringrequiredItem description.
quantityfloatoptionalQuantity ordered.
unit_pricefloatoptionalPrice per unit.
total_pricefloatoptionalTotal line price (quantity x unit_price).
unit_of_measurestringoptionalUnit of measure.
currencystringoptionalCurrency code.
delivery_lead_time_daysintegeroptionalDelivery lead time in days for this line, as quoted by the supplier. Different items in the same PO can have different lead times.
expected_delivery_datedatetimeoptionalComputed delivery date for this line (PO.awarded_at + the line's delivery_lead_time_days). Null if either input is missing.
external_line_idstringoptionalOriginal line ID from your ERP - use this to match PO items back to your requisition lines.
buyer_codestringoptionalInternal code in your system, exactly as you sent it when importing the requisition (e.g. "MAT-001234").
buyer_code_systemstringoptionalSource system of the internal code (e.g. "SAP", "JDE", "Calipso").
buyer_code_descriptionstringoptionalDescription of the internal code.
manufacturer_codestringoptionalManufacturer part number.
manufacturer_namestringoptionalManufacturer name (e.g. "Siemens").
manufacturer_descriptionstringoptionalManufacturer's description of the part.
technical_specsstringoptionalFree-text technical specifications (e.g. "220V, 50Hz, IP55").
requirementsstringoptionalAdditional requirements (e.g. "ISO 9001 certification required").
About delivery dates and lead times
  • Lead time exists at two levels: delivery_lead_time_days at the header (the supplier's headline lead time) and items[].delivery_lead_time_days per line item (the precise per-item value). When the supplier quotes different lead times per line, prefer the per-line value.
  • expected_delivery_date is a computed field: awarded_at + delivery_lead_time_days. Sourced computes it; the supplier does not send a date directly.
  • If delivery_lead_time_days is null, expected_delivery_date will be null too. This typically means the supplier did not include a lead time in their quote.

Download PO Legajo (Audit Dossier)

GET/api/v1/purchase-orders/{po_id}/legajoAPI Key Required

Get a temporary link to download the legajo - a ZIP with the complete audit trail of the award: comparison PDF/XLSX(es), full email history, every participating supplier's attachments, the activity log and the internal logbook (notes flagged for the package).

Request
curl -H "X-API-Key: sk_live_abc123..." \
  https://api.gosourced.ai/api/v1/purchase-orders/567/legajo

Response object

FieldTypeRequiredDescription
download_urlstringrequiredTemporary presigned URL to download the legajo ZIP.
filenamestringrequiredSuggested filename for the ZIP.
expires_inintegerrequiredSeconds until the download URL expires.
size_bytesintegerrequiredSize of the legajo ZIP in bytes.
How the legajo works
  • The response is a temporary presigned URL (valid ~10 minutes). Download the ZIP directly from it - no API key is needed on that URL.
  • The legajo is scoped to the Purchase Request behind the PO. In a split award (several POs from one request), sibling POs return the same dossier.

PO Quotation Comparison

GET/api/v1/purchase-orders/{po_id}/quotationsAPI Key Required

Get the full quotation comparison behind this award: every line quoted by every supplier for the Purchase Request behind the PO, awarded and non-awarded - not just the ones that ended up on this specific PO.

Request
curl -H "X-API-Key: sk_live_abc123..." \
  https://api.gosourced.ai/api/v1/purchase-orders/567/quotations
Response - 200 OK
{
  "purchase_order_id": 567,
  "purchase_order_code": "PO-4F2A91C3",
  "requisition_external_id": "REQ-2026-0042",
  "exchange_rate": {
    "date": "2026-07-12",
    "base": "USD",
    "rates": { "ARS": 1450.5, "EUR": 0.92 }
  },
  "currencies": ["ARS", "USD"],
  "items": [
    {
      "material_code": "MAT-000123",
      "external_line_id": "10",
      "description": "Guantes de nitrilo talle L",
      "quantity": 500.0,
      "quotes": [
        {
          "quotation_id": 8123,
          "supplier_id": 42,
          "supplier_name": "Proveedor A S.A.",
          "supplier_tax_id": "30516242775",
          "supplier_erp_code": "PROV-001",
          "unit_price": 11.25,
          "original_price": 12.5,
          "discount": { "type": "percentage", "value": 10, "source": "manual" },
          "currency": "ARS",
          "quantity": 500,
          "awarded": true,
          "awarded_po_code": "PO-4F2A91C3",
          "lead_time_days": 7,
          "payment_term_code": "NET30",
          "response_status": "PARTIAL",
          "received_at": "2026-07-10T14:32:00+00:00"
        },
        {
          "quotation_id": 8124,
          "supplier_id": 57,
          "supplier_name": "Proveedor B S.R.L.",
          "supplier_tax_id": "30709876541",
          "supplier_erp_code": null,
          "unit_price": 0.0095,
          "original_price": 0.0095,
          "discount": null,
          "currency": "USD",
          "quantity": 500,
          "awarded": false,
          "awarded_po_code": null,
          "lead_time_days": 15,
          "payment_term_code": null,
          "response_status": "INCOMPLETE",
          "received_at": "2026-07-11T09:05:00+00:00"
        }
      ]
    }
  ]
}

Response Object

FieldTypeRequiredDescription
purchase_order_idintegerrequiredID of the queried PO.
purchase_order_codestringrequiredCode of the queried PO (e.g. PO-4F2A91C3).
requisition_external_idstringoptionalExternal ID of the source requisition (ERP system), if any.
exchange_rateobjectoptionalExchange rates stored on the PO at award time, to convert quotes to a common currency: { date, base: "USD", rates: { ARS: 1450.5, EUR: 0.92 } } (units of each currency per 1 USD). null when the PO stored no snapshot (tender, catalog or POs older than this feature).
currenciesarrayrequiredDistinct currencies appearing in the quotes, sorted (e.g. ["ARS", "USD"]).
itemsarrayrequiredLines of the Purchase Request behind the PO, each with its quotes.

Item Object (items[])

FieldTypeRequiredDescription
material_codestringoptionalBuyer material code, if the line has one.
external_line_idstringoptionalLine ID from the source system (ERP), if any.
descriptionstringrequiredDescription of the requested item.
quantityfloatoptionalRequested quantity.
quotesarrayrequiredQuotes received for this line, one per supplier that quoted a price.

Quote Object (items[].quotes[])

FieldTypeRequiredDescription
quotation_idintegerrequiredID of the quotation. A supplier that quoted more than once (rounds) appears once per quotation.
supplier_idintegeroptionalInternal supplier ID in Sourced. Stable identity across lines (names may repeat).
supplier_namestringoptionalName of the quoting supplier.
supplier_tax_idstringoptionalSupplier's tax ID (CUIT in Argentina, CNPJ in Brazil, RUT in Chile/Uruguay).
supplier_erp_codestringoptionalSupplier's ERP code in your organization, if configured.
unit_pricefloatoptionalFINAL unit price, net of discount, in the supplier's original currency. Same number the buyer sees in the award comparison and in the dossier Excel, and the price the PO was awarded at.
original_pricefloatoptionalUnit price before discount. Equals unit_price when there was no discount.
discountobjectoptionalApplied discount: { type: "percentage" | "fixed", value, source }. source: supplier_quoted (offered by the supplier), negotiation, manual (entered by the buyer) or global. null if there was no discount.
currencystringoptionalQuote currency (ISO code, e.g. ARS, USD). When the supplier did not state a currency anywhere, "USD" is reported.
quantityfloatoptionalQuantity quoted by the supplier.
awardedbooleanrequiredtrue if this line was awarded to this supplier on any active PO of the request.
awarded_po_codestringoptionalCode of the PO where the line was awarded (may be a sibling PO in a split award). null if not awarded.
lead_time_daysintegeroptionalOffered delivery lead time, in days (integer). A non-numeric lead time is reported as null.
payment_term_codestringoptionalPayment term code offered by the supplier.
response_statusstringoptionalAI analysis classification of the supplier's response. Values such as INCOMPLETE, PARTIAL, NEEDS_HUMAN_REVIEW, EXPLICIT_REJECTION.
received_atdatetimeoptionalDate and time the quote was received (ISO 8601).
How to read the comparison
  • The comparison is scoped to the Purchase Request behind the PO: it includes quotes from every participating supplier, including the ones that did not win. A line quoted without a price produces no entry in quotes; a catalog PO (no quotation process) returns empty quotes.
  • awarded is request-level: in a split award, a line may have been awarded on a sibling PO different from the one queried - awarded_po_code always holds the code of the PO that won that line.
  • Prices are the same the buyer saw in the award comparison and the ones printed in the dossier Excel: unit_price already includes any negotiated discount (original_price and discount show the gross and the discount). An imported quotation the user has not confirmed yet is not included.
  • Prices are returned in each supplier's quoted currency - no conversion is applied. exchange_rate carries the rates stored on the PO at award time (null if none) so you can convert everything to a common currency.
  • Every key is always present; missing data comes back as null (the response shape never changes).

Update Purchase Order Status

POST/api/v1/purchase-orders/{po_id}/statusAPI Key Required

Update a purchase order's status to reflect its progress in your external system (ERP).

Allowed status transitions:

FromToMeaning
DRAFTCREATEDThe PO was created in your ERP.
CREATEDCONFIRMEDThe PO was fully approved in your ERP.
DRAFTCONFIRMEDShortcut when no intermediate step is needed.
DRAFTREJECTEDPO was rejected in your ERP. The PR is reopened in Sourced.
CREATEDREJECTEDPO was rejected in your ERP after being created. The PR is reopened in Sourced.
DRAFT / CREATED / SENT / CONFIRMEDCANCELLEDThe purchase is not happening. The PO is cancelled and so is the PR - nothing reopens. Use REJECTED instead if the need still exists and you want to re-award.

REJECTED and CANCELLED also work when the PO was a partial award, as long as it is the only active PO on its PR. If the PR has several active POs (a split award across suppliers), the endpoint returns 409 and the revert must be handled within Sourced.

FieldTypeRequiredDescription
statusstringrequiredTarget status: CREATED (PO created in your ERP), CONFIRMED (PO fully approved in your ERP), REJECTED (PO rejected, reopens the PR) or CANCELLED (purchase dropped, cancels both the PO and the PR).
external_idstringoptionalPO number or code in your ERP (e.g., OC-CAL-00045678). Stored for traceability.
notesstringoptionalOptional notes about the status change.
Request
curl -X POST https://api.gosourced.ai/api/v1/purchase-orders/567/status \
  -H "X-API-Key: sk_live_abc123..." \
  -H "Content-Type: application/json" \
  -d '{
    "status": "CREATED",
    "external_id": "OC-CAL-00045678",
    "notes": "Creada en Calipso"
  }'
Response - 200 OK
{
  "status": "created",
  "po_id": 567,
  "code": "PO-A1B2C3D4"
}

Edit a Purchase Order

PATCH/api/v1/purchase-orders/{po_id}API Key Required

Update a Purchase Order's editable fields. Only the fields present in the body are touched: your ERP's PO number, payment term, delivery lead time, observations and status (same state machine as POST /status).

Request Body

FieldTypeRequiredDescription
external_idstringoptionalPO number or code in your ERP. Overwrites the stored value.
payment_terms_codestringoptionalPayment term code from the catalog (e.g. NET30). Case-insensitive; validated against active codes - an unknown code returns 400 with the allowed list.
delivery_lead_time_daysintegeroptionalDelivery lead time in days from PO confirmation. expected_delivery_date derives from this value.
observationsstringoptionalFree text shown on the PO. An empty string clears it.
statusstringoptionalTarget status: CREATED, CONFIRMED, REJECTED or CANCELLED - same transitions as POST /status. Sending the current status is a no-op. REJECTED/CANCELLED return those flows' contract (pr_reopened / pr_code) plus updated_fields.
notesstringoptionalNotes about the status change. Only valid together with status.
Request
curl -X PATCH https://api.gosourced.ai/api/v1/purchase-orders/567 \
  -H "X-API-Key: sk_live_abc123..." \
  -H "Content-Type: application/json" \
  -d '{
    "external_id": "OC-CAL-00045678",
    "payment_terms_code": "NET30",
    "status": "CONFIRMED"
  }'
Response - 200 OK
{
  "status": "updated",
  "po_id": 567,
  "code": "PO-A1B2C3D4",
  "updated_fields": ["external_id", "payment_terms_code", "status"]
}
PATCH semantics
  • True partial update: only the fields present in the body are applied. Explicit null is an error (400) - omit a field to leave it unchanged.
  • All validation runs before anything is applied: on a 400 or 409 response, no field in the body was modified.
  • Status changes use the same transition machine as POST /purchase-orders/{po_id}/status (which remains available). An invalid transition returns 409. Editing a CANCELLED or REJECTED PO returns 409.
  • Changing the payment term or delivery lead time does not regenerate the PDF the supplier already received: the data is updated in Sourced, but the sent document does not change.
  • The award content (prices, items, currency, supplier) is not editable: to fix it, reject the PO (status REJECTED) and re-award.

List Suppliers

GET/api/v1/suppliersAPI Key Required

List suppliers for your organization. Use ?has_erp_code=false to find unmapped suppliers. Use ?detail=full for contacts, categories, and coverage.

Query Parameters

FieldTypeRequiredDescription
pageintegeroptionalPage number (default: 1).
page_sizeintegeroptionalResults per page (1-200, default: 50).
searchstringoptionalSearch by name, custom name, email, tax ID, or ERP code.
erp_codestringoptionalFilter by exact ERP code.
has_erp_codebooleanoptionaltrue = only mapped to ERP, false = only unmapped.
detailstringoptionalSet to 'full' to include contacts, categories, and coverage.
Request
# Light (default)
curl -H "X-API-Key: sk_live_abc123..." \
  "https://api.gosourced.ai/api/v1/suppliers?has_erp_code=false"

# Full detail
curl -H "X-API-Key: sk_live_abc123..." \
  "https://api.gosourced.ai/api/v1/suppliers?search=30712345678&detail=full"
Response - 200 OK
// Light response
{
  "data": [
    {
      "id": 42,
      "name": "Distribuidora Industrial SA",
      "email": "ventas@distribuidora.com",
      "tax_id": "30712345678",
      "country_code": "AR",
      "city": "Buenos Aires",
      "is_active": true,
      "is_preferred": true,
      "erp_code": "PROV-CAL-00123",
      "erp_type": "SAP_B1"
    }
  ],
  "total": 85,
  "page": 1,
  "page_size": 50,
  "has_more": true
}

// Full response (?detail=full)
{
  "data": [
    {
      "id": 42,
      "name": "Distribuidora Industrial SA",
      "email": "ventas@distribuidora.com",
      "tax_id": "30712345678",
      "country_code": "AR",
      "city": "Buenos Aires",
      "is_active": true,
      "is_preferred": true,
      "erp_code": "PROV-CAL-00123",
      "erp_type": "SAP_B1",
      "address": "Av. Corrientes 1234",
      "state_code": "CABA",
      "website": "https://distribuidora.com",
      "description": "Distribuidor de insumos industriales",
      "tax_regime": null,
      "employee_count": "50+",
      "years_in_business": "5+",
      "performance_score": 4.2,
      "reliability_score": 4.5,
      "quality_score": 4.0,
      "contacts": [
        {
          "name": "Juan Pérez",
          "email": "juan@distribuidora.com",
          "phone": "+54 11 5555-1234",
          "role": "SALES",
          "is_primary": true
        }
      ],
      "categories": [
        { "id": 15, "code": "IND-001", "name": "Insumos Industriales", "level": "LEVEL_1" }
      ],
      "coverage": [
        { "country": "Argentina", "province": null, "is_nationwide": true }
      ]
    }
  ],
  "total": 1,
  "page": 1,
  "page_size": 50,
  "has_more": false
}

Create Supplier

POST/api/v1/suppliersAPI Key Required

Create a supplier for your organization. At least one of tax_id or erp_code is required. At least one contact with role 'sales' is required. If a global supplier with the same tax ID already exists, it is reused and linked to your organization.

Request Body

FieldTypeRequiredDescription
namestringrequiredSupplier name for your organization.
tax_idstringoptionalTax ID (CUIT, CNPJ, RUT, etc.). At least one of tax_id or erp_code is required.
erp_codestringoptionalSupplier code in your ERP. At least one of tax_id or erp_code is required.
erp_typestringoptionalERP type (SAP_B1, JDE, ORACLE_CLOUD, etc.).
country_codestringoptionalISO 3166-1 country code (e.g., AR, BR, US).
citystringoptionalSupplier city.
addressstringoptionalFull address.
state_codestringoptionalState/province (e.g., CABA, SP).
websitestringoptionalSupplier website.
contactsarrayrequiredList of contacts. At least one with role 'sales' is required.
contacts[].namestringrequiredContact name.
contacts[].emailstringrequiredContact email.
contacts[].phonestringoptionalPhone (optional).
contacts[].rolestringrequiredRole: SALES or LOGISTICS.
contacts[].is_primarybooleanoptionaltrue if primary contact (default: false).
Request
curl -X POST https://api.gosourced.ai/api/v1/suppliers \
  -H "X-API-Key: sk_live_abc123..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Distribuidora Industrial SA",
    "tax_id": "30712345678",
    "erp_code": "PROV-CAL-00123",
    "erp_type": "SAP_B1",
    "country_code": "AR",
    "city": "Buenos Aires",
    "address": "Av. Corrientes 1234",
    "contacts": [
      {
        "name": "Juan Pérez",
        "email": "juan@distribuidora.com",
        "phone": "+54 11 5555-1234",
        "role": "SALES",
        "is_primary": true
      },
      {
        "name": "María García",
        "email": "maria@distribuidora.com",
        "role": "LOGISTICS"
      }
    ]
  }'
Response - 200 OK
// 201 Created
{
  "id": 42,
  "name": "Distribuidora Industrial SA",
  "email": null,
  "tax_id": "30712345678",
  "country_code": "AR",
  "city": "Buenos Aires",
  "is_active": true,
  "is_preferred": false,
  "erp_code": "PROV-CAL-00123",
  "erp_type": "SAP_B1",
  "address": "Av. Corrientes 1234",
  "state_code": null,
  "website": null,
  "description": null,
  "tax_regime": null,
  "employee_count": null,
  "years_in_business": null,
  "performance_score": null,
  "reliability_score": null,
  "quality_score": null,
  "contacts": [
    {
      "name": "Juan Pérez",
      "email": "juan@distribuidora.com",
      "phone": "+54 11 5555-1234",
      "role": "SALES",
      "is_primary": true
    },
    {
      "name": "María García",
      "email": "maria@distribuidora.com",
      "phone": null,
      "role": "LOGISTICS",
      "is_primary": false
    }
  ],
  "categories": [],
  "coverage": []
}

Create or update delivery trackings

POST/api/v1/delivery-trackingsAPI Key Required

Bulk upsert of 1 to 200 PO lines to track for delivery. The natural key is (po_code, line_position): resending the same line updates the existing tracking instead of duplicating it.

Rate limit: 30 requests per minute (the other endpoints in this group use the general 60/minute limit).

supplier_erp_code is the recommended identifier for matching the supplier. supplier_tax_id is a fallback. supplier_name is display-only text: it is never used for matching.
A line whose supplier could not be resolved is left in SUPPLIER_NOT_FOUND: no email is sent for it. Once the supplier is created with POST /suppliers, resend the same line (same po_code and line_position) so it gets linked.

Request body

The body must contain a lines array with 1 to 200 lines. Each line carries the PO, the position, the promised date, the supplier (ERP code or tax ID) and the item detail (description, ordered quantity and pending quantity): without those there is nothing to follow up and nothing to show the supplier. A missing required field rejects the whole batch with 422.

Line object (lines[])

FieldTypeRequiredDescription
po_codestringrequiredPO number in the customer's system. 100 characters max.
line_positionintegerrequiredLine position within the PO (10, 20, 30…). Integer ≥ 0.
original_delivery_datedaterequiredCommitted delivery date (YYYY-MM-DD)
supplier_erp_codestringOne of the twoSupplier code in the client's ERP. Recommended identifier. Required if supplier_tax_id is not sent. Max 100 characters.
supplier_tax_idstringOne of the twoSupplier tax ID (CUIT/RUT). Required if supplier_erp_code is not sent. Max 50 characters.
supplier_namestringoptionalDisplay-only text; never used for matching the supplier. 255 characters max.
material_codestringoptionalMaterial/item code in the customer's system. 100 characters max.
descriptionstringrequiredItem description. This is what the supplier sees in the follow-up email. Max 500 characters.
quantity_orderedfloatrequiredOrdered quantity. ≥ 0. This is what the supplier sees in the follow-up email.
quantity_pendingfloatrequiredQuantity pending delivery. ≥ 0. This is what the email asks the supplier for: with partial deliveries, send what is still missing, not what was ordered.
unit_of_measurestringoptionalUnit of measure. 20 characters max.
po_datedateoptionalPO date (YYYY-MM-DD).
buyer_emailstringoptionalEscalation CC and recipient of delay alerts. If not sent, the in-app delay notification goes to the organization's first logistics-role user (one, not all of them).
is_urgentbooleanoptionalMarks the line as urgent.

You must send at least supplier_erp_code or supplier_tax_id. For supplier_tax_id, an empty value, or one made only of separators (for example "-" or " - "), counts as not sent, and matching ignores dashes and spaces on both sides (30-71234567-8 matches 30712345678). supplier_erp_code is only trimmed: a literal "-" DOES count as sent and is used as-is to look up a supplier — if it matches none, the line is created as SUPPLIER_NOT_FOUND (not an error).

Upsert rules

  • New line: created as PENDING_SCHEDULE if the supplier resolved, or SUPPLIER_NOT_FOUND otherwise. It starts active (not paused).
  • Existing line, open, created by this same API: its data and quantities are updated. A change to original_delivery_date or to the quantities does not restart the follow-up cycle.
  • Existing line already closed (DELIVERED or CANCELLED): it is reactivated with a fresh follow-up cycle (confirmation emails are sent again); the result is reactivated and counts toward updated_count.
  • Correcting the supplier on an existing line: if it arrives with a supplier_erp_code or supplier_tax_id that resolves to a DIFFERENT supplier, the tracking is re-linked to that company and the cycle starts over (the previous emails went to the wrong company). A line that does not resolve any supplier never unlinks the supplier already assigned.
  • No changes: the result is unchanged; no event is written and updated_at is not touched.
  • Line managed by another feed (CSV or ERP sync): rejected with the dlvTrackingManagedByOtherSource error. Trackings loaded via CSV or the ERP sync cannot be modified through this API.
  • Pending quantity 0: the line was already received in full. On an open tracking it is closed as DELIVERED (delivered event with reason quantity_pending_zero; the actual date is set to the day of the request — if you know it, use mark_delivered with actual_delivery_date). On an already closed one it is unchanged, never a reactivation. A NEW line with 0 pending returns the dlvNothingPending error.
Request
curl -X POST https://api.gosourced.ai/api/v1/delivery-trackings \
  -H "X-API-Key: sk_live_abc123..." \
  -H "Content-Type: application/json" \
  -d '{
    "lines": [
      {
        "po_code": "OC-2026-0455",
        "line_position": 10,
        "original_delivery_date": "2026-10-15",
        "supplier_erp_code": "PROV-CAL-00123",
        "material_code": "MAT-001234",
        "description": "Rodamiento SKF 6205",
        "quantity_ordered": 50,
        "quantity_pending": 50,
        "unit_of_measure": "UN",
        "buyer_email": "compras@empresa.com"
      },
      {
        "po_code": "OC-2026-0455",
        "line_position": 20,
        "original_delivery_date": "2026-10-15",
        "supplier_tax_id": "30-71234567-8",
        "description": "Filtro de aceite",
        "quantity_ordered": 12,
        "quantity_pending": 12,
        "unit_of_measure": "UN"
      }
    ]
  }'

Response

Upsert response

FieldTypeRequiredDescription
created_countintegerrequiredNumber of lines created.
updated_countintegerrequiredNumber of lines updated (includes reactivated ones).
unchanged_countintegerrequiredNumber of lines with no changes.
error_countintegerrequiredNumber of lines with an error.
resultsarrayrequiredResult for each line in the batch, in the same order they were sent.

Per-line result object (results[])

FieldTypeRequiredDescription
po_codestringrequiredPO number, as sent.
line_positionintegerrequiredLine position, as sent.
idintegeroptionalInternal tracking id. Absent when the line errored.
resultstringrequiredcreated | updated | unchanged | reactivated | error.
statusstringoptionalTracking status after the upsert. Absent on error lines.
codestringoptionalError code (dlv*). Only present when result is error.
messagestringoptionalEnglish error detail, meant for logs. Don't translate or show it to the end user: code is the i18n key.
Response - 200 OK
// 200 OK
{
  "created_count": 1,
  "updated_count": 0,
  "unchanged_count": 0,
  "error_count": 1,
  "results": [
    {
      "po_code": "OC-2026-0455",
      "line_position": 10,
      "id": 8821,
      "result": "created",
      "status": "PENDING_SCHEDULE"
    },
    {
      "po_code": "OC-2026-0455",
      "line_position": 20,
      "result": "error",
      "code": "dlvSupplierReferenceAmbiguous",
      "message": "supplier_tax_id '30-71234567-8' matches more than one supplier"
    }
  ]
}

The HTTP status is always 200, even when some lines error out: check error_count and each line's code in results to see what failed.

List delivery trackings

GET/api/v1/delivery-trackingsAPI Key Required

Get your organization's delivery trackings, from every source (API, CSV and ERP sync). Supports incremental sync via since.

Query Parameters

FieldTypeRequiredDescription
pageintegeroptionalPage number (default: 1).
page_sizeintegeroptionalItems per page, 1-200 (default: 50).
statusstringoptionalFilter by status (see the status table below). Includes STAND_BY.
po_codestringoptionalFilter by exact po_code.
supplier_erp_codestringoptionalFilter by the supplier's ERP code.
sincedatetimeoptionalISO 8601 timestamp. Returns only trackings created or modified since that date, ordered ascending by modification date. Without a timezone offset it's interpreted as UTC: always send an explicit offset (or "Z") to avoid ambiguity.
open_onlybooleanoptionalExcludes DELIVERED and CANCELLED trackings.
With since, results are ordered by modification date ascending (not by creation date descending, which is the default order): save the updated_at of the last record you received and use it as the next since. So that rows written by a long transaction (a large CSV, an ERP sync) are never missed, the server moves the since 10 minutes back: you will receive records from those minutes again, so deduplicate by id (the upsert is idempotent on your side). For a tracking that was never modified, updated_at equals created_at (never null): the cursor always has a usable value, no fallback needed on your side.

Statuses

StatusDescription
PENDING_SCHEDULEPending schedule. Loaded; a confirmation has not been requested from the supplier yet.
READY_TO_SENDReady to send. Ready to send the delivery confirmation request.
SENT_PENDING_RESPONSESent, awaiting response. Confirmation was requested; the supplier hasn't replied yet.
NO_RESPONSENo response. No reply after the reminders.
CONFIRMED_ON_TIMEConfirmed on time. The supplier confirmed the original date.
CONFIRMED_DELAYEDConfirmed delayed. Confirmed, but with a date later than the original.
CONFIRMED_EARLYConfirmed early. Confirmed a date earlier than the original.
REQUIRES_REVIEWRequires review. The reply was ambiguous or changed conditions; needs a buyer's review.
DELIVEREDDelivered. Already delivered.
CANCELLEDCancelled. The PO was cancelled.
SUPPLIER_NOT_FOUNDSupplier not identified. The supplier could not be matched (by ERP code or tax id).
STAND_BYManually paused and still open: no email is sent for it. This is a virtual status — a terminal status (DELIVERED/CANCELLED) always wins over STAND_BY, so a paused line that gets delivered or cancelled is reported under its real status.
Request - Incremental sync
curl -H "X-API-Key: sk_live_abc123..." \
  "https://api.gosourced.ai/api/v1/delivery-trackings?since=2026-09-01T00:00:00Z&open_only=true"

Delivery tracking object

FieldTypeRequiredDescription
idintegerrequiredInternal tracking id.
po_codestringrequiredPO number in the customer's system.
line_positionintegerrequiredLine position within the PO.
sourcestringrequiredWhere the tracking came from: api, csv or erp_sync. This endpoint reads trackings from all three sources.
supplierobjectoptionalLinked supplier, or null if it could not be resolved yet (SUPPLIER_NOT_FOUND).
supplier.idintegeroptionalInternal supplier id.
supplier.namestringoptionalSupplier's legal name.
supplier.tax_idstringoptionalSupplier's tax id.
supplier.erp_codestringoptionalSupplier's ERP code for your organization.
supplier_namestringoptionalSupplier name as loaded (may differ from supplier.name if it only arrived as free text).
material_codestringoptionalMaterial/item code.
descriptionstringoptionalItem description.
quantity_orderedfloatoptionalOrdered quantity.
quantity_pendingfloatoptionalQuantity pending delivery.
unit_of_measurestringoptionalUnit of measure.
po_datedateoptionalPO date.
original_delivery_datedateoptionalOriginally committed delivery date.
etadateoptionalEstimated delivery date. A buyer fills it in by hand from the app (the detail modal); no automated process computes it. On an API-created line it stays null until someone enters it.
actual_delivery_datedateoptionalActual delivery date, once status is DELIVERED.
received_quantityfloatoptionalQuantity received, once status is DELIVERED.
statusstringoptionalPublic status (see the status table).
is_urgentbooleanrequiredWhether the line is marked urgent.
pausedbooleanrequiredWhether follow-up sending is paused (sending_paused).
pause_reasonstringoptionalReason for the pause. null when not paused.
followup_countintegerrequiredNumber of reminders sent in the current cycle.
last_followup_sent_atdatetimeoptionalDate and time of the last reminder sent.
next_followup_datedatetimeoptionalPlanned date for the next reminder.
supplier_responseobjectrequiredThe supplier's latest response.
supplier_response.responded_atdatetimeoptionalDate and time the supplier responded.
supplier_response.confirmed_delivery_datedateoptionalDelivery date the supplier confirmed.
supplier_response.delay_reasonstringoptionalDelay reason, if the supplier gave one.
supplier_response.notesstringoptionalNotes from the supplier's latest response. null if they haven't replied yet.
supplier_response.qualitystringoptionalAI-assessed quality of the response (for example, concrete).
created_atdatetimeoptionalWhen the tracking was created.
updated_atdatetimeoptionalWhen it was last modified.
Response - 200 OK
{
  "data": [
    {
      "id": 8821,
      "po_code": "OC-2026-0455",
      "line_position": 10,
      "source": "api",
      "supplier": {
        "id": 42,
        "name": "Distribuidora Industrial SA",
        "tax_id": "30712345678",
        "erp_code": "PROV-CAL-00123"
      },
      "supplier_name": "Distribuidora Industrial SA",
      "material_code": "MAT-001234",
      "description": "Rodamiento SKF 6205",
      "quantity_ordered": 50,
      "quantity_pending": 20,
      "unit_of_measure": "UN",
      "po_date": null,
      "original_delivery_date": "2026-10-15",
      "eta": "2026-10-18",
      "actual_delivery_date": null,
      "received_quantity": null,
      "status": "CONFIRMED_DELAYED",
      "is_urgent": false,
      "paused": false,
      "pause_reason": null,
      "followup_count": 2,
      "last_followup_sent_at": "2026-09-10T13:05:00+00:00",
      "next_followup_date": "2026-09-17T00:00:00+00:00",
      "supplier_response": {
        "responded_at": "2026-09-12T09:40:00+00:00",
        "confirmed_delivery_date": "2026-10-18",
        "delay_reason": "Demora del fabricante",
        "notes": "Se despachó el lote parcial, el resto llega la semana próxima.",
        "quality": "concrete"
      },
      "created_at": "2026-09-01T12:00:00+00:00",
      "updated_at": "2026-09-12T09:40:00+00:00"
    }
  ],
  "total": 1,
  "page": 1,
  "page_size": 50,
  "has_more": false
}

Get a delivery tracking

GET/api/v1/delivery-trackings/{id}API Key Required

Get the detail of a tracking by its internal id.

Same shape as each element of GET /delivery-trackings.

Request
curl -H "X-API-Key: sk_live_abc123..." \
  https://api.gosourced.ai/api/v1/delivery-trackings/8821
Response - 200 OK
{
  "id": 8821,
  "po_code": "OC-2026-0455",
  "line_position": 10,
  "source": "api",
  "supplier": {
    "id": 42,
    "name": "Distribuidora Industrial SA",
    "tax_id": "30712345678",
    "erp_code": "PROV-CAL-00123"
  },
  "supplier_name": "Distribuidora Industrial SA",
  "material_code": "MAT-001234",
  "description": "Rodamiento SKF 6205",
  "quantity_ordered": 50,
  "quantity_pending": 20,
  "unit_of_measure": "UN",
  "po_date": null,
  "original_delivery_date": "2026-10-15",
  "eta": "2026-10-18",
  "actual_delivery_date": null,
  "received_quantity": null,
  "status": "CONFIRMED_DELAYED",
  "is_urgent": false,
  "paused": false,
  "pause_reason": null,
  "followup_count": 2,
  "last_followup_sent_at": "2026-09-10T13:05:00+00:00",
  "next_followup_date": "2026-09-17T00:00:00+00:00",
  "supplier_response": {
    "responded_at": "2026-09-12T09:40:00+00:00",
    "confirmed_delivery_date": "2026-10-18",
    "delay_reason": "Demora del fabricante",
    "notes": "Se despachó el lote parcial, el resto llega la semana próxima.",
    "quality": "concrete"
  },
  "created_at": "2026-09-01T12:00:00+00:00",
  "updated_at": "2026-09-12T09:40:00+00:00"
}

An id that doesn't exist, or that belongs to another organization, returns 404 dlvTrackingNotFound: the existence of a tracking that isn't yours is never confirmed.

Delivery tracking event history

GET/api/v1/delivery-trackings/{id}/eventsAPI Key Required

Paginated timeline of a tracking's events, oldest first.

Query Parameters

FieldTypeRequiredDescription
pageintegeroptionalPage number (default: 1).
page_sizeintegeroptionalItems per page, 1-200 (default: 50).

Event object

FieldTypeRequiredDescription
idintegerrequiredInternal event id.
typestringrequiredEvent type (see the type table).
occurred_atdatetimeoptionalWhen it happened.
actor_typestringrequiredWho generated it: system, supplier, user or api.
dataobjectrequiredEvent detail; the shape depends on type (see the type table).

Event types

Typedata
created{ source }
reactivated{ source } (the API path also adds from with the previous status)
cycle_reset{ reason? } — follow-up cycle reset without a reactivation (supplier change via API, manual reset from the app): the supplier response prior to this event is no longer the current one
line_updated{ <field>: { from, to } } — one key per changed field (exception: buyer_email carries only to, no from)
followup_sent{ followup_number, escalated }
response_received{ confirmed_delivery_date, delay_reason, notes, quality, summary, outcome }
status_changed{ from, to, reason? }
paused{ reason }
resumed{ reason? }
urgency_changed{ is_urgent }
delivered{ from, to, reason?, actual_delivery_date, received_quantity } (a snapshot closure also adds snapshot_id)
cancelled{ from, to, reason? }
The note_added event (the buyer's manual notes from the UI) and the actor_ref field never come out through this API.
The history starts on this feature's release date: trackings that already existed before it have no events prior to that point (no backfill).
Response - 200 OK
{
  "data": [
    {
      "id": 55009,
      "type": "created",
      "occurred_at": "2026-09-01T12:00:00+00:00",
      "actor_type": "api",
      "data": { "source": "api" }
    },
    {
      "id": 55012,
      "type": "status_changed",
      "occurred_at": "2026-09-12T09:40:00+00:00",
      "actor_type": "supplier",
      "data": {
        "from": "SENT_PENDING_RESPONSE",
        "to": "CONFIRMED_DELAYED",
        "reason": "supplier_response"
      }
    }
  ],
  "total": 2,
  "page": 1,
  "page_size": 50,
  "has_more": false
}

Bulk actions

POST/api/v1/delivery-trackings/actionsAPI Key Required

Apply an action to up to 200 lines at once. If a target omits line_position, the action applies to every open line of that PO.

Request Body

FieldTypeRequiredDescription
actionstringrequiredmark_delivered, cancel, pause, resume, set_urgent or unset_urgent.
targetsarrayrequiredLines to apply the action to. Between 1 and 200.
targets[].po_codestringrequiredPO number. 100 characters max.
targets[].line_positionintegeroptionalLine position. If omitted, applies to every open line of the PO.
reasonstringoptionalReason. Required for pause; optional otherwise. 500 characters max.
actual_delivery_datedateoptionalActual delivery date. Only used with mark_delivered; defaults to today.
received_quantityfloatoptionalReceived quantity. Only used with mark_delivered. ≥ 0.
Omitting targets[].line_position applies the action to every open line (not delivered, not cancelled) of that PO.

Available actions

actionEffectRequires
mark_deliveredMoves to DELIVERED. actual_delivery_date uses the sent date or, if not sent, the organization's today. received_quantity is stored when sent.—
cancelMoves to CANCELLED.—
pausePauses follow-up emails (sending_paused = true).reason
resumeResumes follow-up emails (sending_paused = false).—
set_urgentMarks the line as urgent.—
unset_urgentUnmarks the line as urgent.—
  • Actions are idempotent: repeating an action that already applied doesn't write a new event and returns one of these six results: already_delivered, already_cancelled, already_paused, already_active (for resume), already_urgent or already_not_urgent.
  • You can't cross between terminal statuses: mark_delivered on a CANCELLED line, or cancel on a DELIVERED line, returns the dlvActionNotAllowedInStatus error. To reopen a closed line, resend it through POST /delivery-trackings (the upsert does reactivate it).
  • mark_delivered and cancel only apply to lines created by this API (source = "api"); on CSV or ERP sync lines they return dlvTrackingManagedByOtherSource. pause, resume, set_urgent and unset_urgent apply to lines from any source, as long as they aren't in a terminal status.
Request
curl -X POST https://api.gosourced.ai/api/v1/delivery-trackings/actions \
  -H "X-API-Key: sk_live_abc123..." \
  -H "Content-Type: application/json" \
  -d '{
    "action": "mark_delivered",
    "targets": [
      { "po_code": "OC-2026-0455", "line_position": 10 }
    ],
    "received_quantity": 50
  }'

Actions response

FieldTypeRequiredDescription
applied_countintegerrequiredNumber of trackings (not request targets) the action was applied to. already_* results don't count here.
error_countintegerrequiredNumber of trackings (not request targets) with an error. already_* results don't count here.
resultsarrayrequiredResult for each target.

Per-target result object (results[])

FieldTypeRequiredDescription
po_codestringrequiredPO number.
line_positionintegeroptionalAffected line position. May differ from the target if it was omitted (a whole PO can produce several results).
idintegeroptionalInternal tracking id. Absent when the target doesn't match any tracking.
resultstringrequiredapplied, error, or one of the six idempotent results: already_delivered, already_cancelled, already_paused, already_active, already_urgent, already_not_urgent.
statusstringoptionalTracking status after the action.
codestringoptionalError code (dlv*). Only present when result is error.
messagestringoptionalEnglish error detail, meant for logs.
Response - 200 OK
// 200 OK
{
  "applied_count": 1,
  "error_count": 0,
  "results": [
    {
      "po_code": "OC-2026-0455",
      "line_position": 10,
      "id": 8821,
      "status": "DELIVERED",
      "result": "applied"
    }
  ]
}

Full pending snapshot

POST/api/v1/delivery-trackings/snapshotsAPI Key Required

Send ALL the lines you have pending delivery in a single request, up to 10,000. The body is the same as POST /delivery-trackings: an object with "lines", and each line with the same fields. The only difference is that here the whole pending list goes together, not split into batches of 200. We respond 202 with an id and process it in the background; check the result with GET /delivery-trackings/snapshots/{id}.

Rate limit: 6 accepted snapshots per hour per API key (enough to send the full pending snapshot once or twice a day). A submission rejected with 422 does not use up the quota. Over the limit we respond 429 dlvSnapshotRateLimited with a Retry-After header.

Important: do not split a snapshot into several requests. Each snapshot is taken as your COMPLETE pending list, so if you send one part and then another, the second one closes as delivered the lines that came in the first.

Request Body

FieldTypeRequiredDescription
linesarrayrequiredAll your pending lines. Each line has exactly the same fields as in POST /delivery-trackings (po_code, line_position, original_delivery_date, description, quantity_ordered, quantity_pending, supplier_erp_code or supplier_tax_id, plus the optional ones). 1 to 10,000.
  • Open lines that were loaded via API and aren't in the snapshot get closed as DELIVERED (a delivered event with reason absent_from_api_snapshot). Lines loaded via CSV or from the ERP aren't touched. The scope is the whole organization: it includes lines loaded via API with any API key and lines sent through POST /delivery-trackings. If you have more than one integration, each snapshot needs to include the pending lines from all of them.
  • All or nothing: if a line is invalid or the same line (po_code + line_position) appears more than once, the whole snapshot is rejected with 422 and nothing is processed.
  • Only the most recent snapshot counts: if you send a new one before the previous one starts processing, the previous one ends up SUPERSEDED and is never applied. If the previous one was already processing, it finishes and the new one is applied right after.
  • Safeguard: if closing lines would close more than 20 lines AND more than half of the open ones, none get closed (closure_skipped = true); creates and updates are still applied. Check that your submission is complete and resend it.
  • The request body can't exceed ~10 MB; with long descriptions, 10,000 lines can go over that: in that case the gateway responds with 413.
  • POST /delivery-trackings (up to 200 lines) is still available for one-off changes and never closes lines by absence.

Moving from POST /delivery-trackings to snapshots

  1. Put into a single array all the lines you currently send in several batches of 200. Each line stays exactly as it is today.
  2. Send that array in a single POST to /delivery-trackings/snapshots and store the id from the response (202).
  3. Poll GET /delivery-trackings/snapshots/{id} every 10 to 15 seconds until status is COMPLETED, FAILED or SUPERSEDED.
  4. On COMPLETED, check errors (lines that could not be applied) and closure_skipped: if it is true, no line was closed because the request looked incomplete.
  5. Repeat it every time your process runs (once or twice a day). Lines that stop coming are closed automatically.
Request
curl -X POST https://api.gosourced.ai/api/v1/delivery-trackings/snapshots \
  -H "X-API-Key: sk_live_abc123..." \
  -H "Content-Type: application/json" \
  -d '{
    "lines": [
      {
        "po_code": "OC-2026-0455", "line_position": 10,
        "original_delivery_date": "2026-10-15",
        "supplier_erp_code": "PROV-0042",
        "description": "Rodamiento 6204-2RS",
        "quantity_ordered": 50, "quantity_pending": 20
      }
    ]
  }'
Response - 202 Accepted
{
  "id": 123,
  "status": "PENDING",
  "total_lines": 3120,
  "received_at": "2026-09-28T13:05:00Z"
}

Snapshot result

GET/api/v1/delivery-trackings/snapshots/{snapshot_id}API Key Required

Status and result of a snapshot. Poll it every few seconds until status is COMPLETED, FAILED or SUPERSEDED.

Snapshot object

FieldTypeRequiredDescription
statusstringrequiredPENDING, PROCESSING, COMPLETED, FAILED or SUPERSEDED.
processed_linesintegerrequiredLines already applied: shows progress.
created_count / updated_count / unchanged_count / error_countintegerrequiredSame counters as the upsert.
closed_countintegerrequiredLines closed because they weren't in the snapshot.
closure_skippedbooleanrequiredtrue if the closure was skipped by the quantity safeguard.
closure_skipped_reasonstringoptionalWith closure_skipped = true, the reason: too_many_absent (the closure would have exceeded the quantity safeguard). Otherwise null.
would_close_countintegerrequiredHow many lines would have been closed.
superseded_byintegeroptionalWith SUPERSEDED, the id of the newer snapshot that replaced it.
error_codestringoptionalWith FAILED, dlvSnapshotProcessingFailed. A FAILED snapshot never closes lines: resending it is safe.
errorsarrayrequiredLines that couldn't be applied, with po_code, line_position, code and message. Those lines count as present: they aren't closed.
Request
curl https://api.gosourced.ai/api/v1/delivery-trackings/snapshots/123 \
  -H "X-API-Key: sk_live_abc123..."
Response - 200 OK
{
  "id": 123,
  "status": "COMPLETED",
  "received_at": "2026-09-28T13:05:00Z",
  "started_at": "2026-09-28T13:05:01Z",
  "completed_at": "2026-09-28T13:05:48Z",
  "total_lines": 3120,
  "processed_lines": 3120,
  "created_count": 14,
  "updated_count": 230,
  "unchanged_count": 2860,
  "error_count": 16,
  "closed_count": 41,
  "would_close_count": 41,
  "closure_skipped": false,
  "closure_skipped_reason": null,
  "superseded_by": null,
  "error_code": null,
  "errors": [
    { "po_code": "OC-2026-0460", "line_position": 20, "code": "dlvSupplierReferenceAmbiguous", "message": "..." }
  ]
}

Error codes — delivery tracking

Codes specific to the delivery tracking endpoints. Some are whole-request errors (HTTP column 400/403/404/422); the rest travel in the code field of each failed line or target, inside a 200 response. A missing or invalid required field (description, quantities, supplier with neither ERP code nor tax ID, pending greater than ordered) has no code of its own: it rejects the whole batch with 422 and the standard validation detail.

CodeHTTPMeaning
dlvFeatureDisabled403Delivery tracking is not enabled for this organization.
dlvTrackingNotFound404Tracking not found (id doesn't exist, or belongs to another organization).
dlvTrackingManagedByOtherSource200This line is managed by another feed (CSV or ERP sync).
dlvSupplierReferenceConflict200The ERP code and tax id resolve to different suppliers.
dlvSupplierReferenceAmbiguous200The ERP code or tax ID matches more than one supplier.
dlvDuplicateLineInBatch200The line (same po_code and line_position) appears more than once in the batch.
dlvNothingPending200The line has nothing pending (quantity_pending = 0): no tracking is created to chase 0 units.
dlvConcurrentUpsert200The line was modified at the same time by another request. Retry.
dlvLineWriteFailed200The line could not be saved. Check the data and try again (not a concurrency issue).
dlvInvalidStatusFilter400The status value is not a valid status.
dlvInvalidSince400Invalid since date. Use ISO 8601 format.
dlvPauseReasonRequired400pause requires a reason.
dlvActionNotAllowedInStatus200The action does not apply to the tracking's current status.
dlvSnapshotDuplicateLines422The snapshot has the same line (po_code + line_position) more than once. params.keys lists the duplicates (up to 50).
dlvSnapshotNotFound404Snapshot not found (id doesn't exist, or belongs to another organization).
dlvSnapshotRateLimited4296 snapshots were already accepted in the last hour with this API key. params.retry_after (and the Retry-After header) carry the seconds until a slot frees up.
dlvSnapshotProcessingFailed200The snapshot could not be processed after 3 attempts. It may have applied part of the lines, but closed none. Resend it. This code never travels in a line's code: it arrives in the snapshot's error_code (GET /delivery-trackings/snapshots/{id}, which responds 200).
200 = travels inside the response, in the code of one specific line or target (the rest of the batch still goes through). 400/403/404 = rejects the whole request. 422 = body validation (missing or invalid required field), also rejects the whole request; dlvSnapshotDuplicateLines is one such 422. dlvTrackingNotFound is both: 404 on the by-id GETs, and per-target inside /actions.

Webhooks

Receive real-time events when a purchase order, a requisition or a delivery tracking changes, instead of having to poll for them.

Setup

Webhooks are configured from the app (not via API key), by an organization_admin user, under My Organization → Developers → Webhooks.

Creating a webhook generates a secret that is shown only once: save it, it's used to verify the signature of every delivery.

Envelope

Every event arrives with this shape:

Event
{
  "id": "evt_5f2a1c9b8e7d4a3f1b2c",
  "type": "delivery_tracking.status_changed",
  "created_at": "2026-09-17T14:05:00+00:00",
  "data": {
    "tracking": "... mismo objeto que devuelve GET /delivery-trackings/{id} ...",
    "event": "... mismo objeto que devuelve GET /delivery-trackings/{id}/events ..."
  }
}

Headers

FieldTypeRequiredDescription
X-Webhook-SignaturestringrequiredHMAC-SHA256 signature of the raw request body, prefixed with sha256=.
X-Webhook-EventstringrequiredThe event's type — the same value as the envelope's top-level type field. There's no data.type: data is the resource itself (or the tracking / event pair for delivery events).
X-Webhook-Delivery-IdstringrequiredThe event's id, not of this particular delivery attempt: it's the same value on every retry and on every subscribed webhook that receives it (same as id in the body) — which is exactly why it works for deduplication.

Verifying the signature

Compute the HMAC-SHA256 over the raw bytes of the request body (before parsing the JSON) using the webhook's secret, and compare it against X-Webhook-Signature with a constant-time comparison.

Python
import hashlib
import hmac

def verify_webhook(secret: str, raw_body: bytes, signature_header: str | None) -> bool:
    if not signature_header or not signature_header.startswith("sha256="):
        return False
    expected = "sha256=" + hmac.new(
        secret.encode(), raw_body, hashlib.sha256
    ).hexdigest()
    # compare_digest on two strings of different length returns False,
    # it never raises — safe even with a malformed header.
    return hmac.compare_digest(expected, signature_header)
Node.js
const crypto = require("crypto");

function verifyWebhook(secret, rawBody, signatureHeader) {
  if (!signatureHeader || !signatureHeader.startsWith("sha256=")) {
    return false;
  }
  const expected =
    "sha256=" +
    crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
  const expectedBuf = Buffer.from(expected);
  const receivedBuf = Buffer.from(signatureHeader);
  // timingSafeEqual throws on a length mismatch — check first, it never
  // needs to be constant-time (the lengths themselves aren't a secret).
  if (expectedBuf.length !== receivedBuf.length) {
    return false;
  }
  return crypto.timingSafeEqual(expectedBuf, receivedBuf);
}
The signature is computed over the RAW body, exactly as it arrives on the wire. If your framework already parsed the JSON before you can access the original bytes, verification will fail even though the signature is correct — you need the unprocessed body.

Retries

If delivery fails (timeout, network error, or a non-2xx response), it's retried up to 3 more times (4 attempts total: the initial send plus 3 retries). Each failed attempt schedules the next one with a minimum backoff of 1, 5 and 15 minutes; that retry only actually fires the next time the retry sweep (reminder-checker) runs, which happens hourly during Argentine business hours — the real time until the next attempt can be longer than those minutes.

  • Attempt 1: immediate, at the time of the event.
  • Attempt 2: no sooner than 1 minute later.
  • Attempt 3: no sooner than 5 minutes later.
  • Attempt 4: no sooner than 15 minutes later. If it fails too, the delivery is marked FAILED.
The same event can arrive more than once (retries, reprocessing). Deduplicate by the event's id (or by the X-Webhook-Delivery-Id header, which is the same value).

Available events

typedataFires when
purchase_order.createdthe PO objectA new purchase order was created.
purchase_order.updatedid, code, status, reasonA purchase order's status or other data changed (for example, a rejection).
requisition.discardedexternal_id, requisition_id, status (discarded | cancelled | partially_cancelled | already_cancelled), pr_id, pr_code (only when the requisition had lines launched to a Purchase Request), discarded_by, discarded_atA requisition was discarded (for example, cancelled from the ERP with POST /requisitions/cancel).
delivery_tracking.response_receivedtracking, eventThe supplier replied to the delivery confirmation request.
delivery_tracking.status_changedtracking, eventThe tracking's status changed (includes reactivating a previously closed line, which goes back to PENDING_SCHEDULE).
delivery_tracking.deliveredtracking, eventThe tracking moved to DELIVERED.
delivery_tracking.cancelledtracking, eventThe tracking moved to CANCELLED.
The exact shape of data on purchase_order.created/updated isn't always the same: it depends on what triggered the event (a split award sends a smaller summary than a full one; some triggers use po_id instead of id, or add updated_fields instead of reason). Treat them as a signal to re-read the resource via GET, not as a fixed contract.
In the webhook payload, the tracking's supplier block does NOT include erp_code (unlike GET /delivery-trackings, which does include it).
Events that originate in a background process (for example, a supplier's reply by email) can take up to an hour to fire. Don't rely on the webhook alone — use GET /delivery-trackings?since= for periodic reconciliation.

Schema Reference

Quick reference of all request/response schemas used across endpoints.

Requisition Statuses

StatusDescription
PENDINGImported, awaiting review by a buyer in Sourced.
LAUNCHEDBuyer has launched the requisition as a Purchase Request.
DISCARDEDRequisition was manually discarded.
SUPERSEDEDA newer version was imported with the same external_id.

Purchase Order Statuses

StatusDescription
DRAFTPO created in Sourced, pending sync to external ERP.
CREATEDPO created in the external ERP, pending approval.
CONFIRMEDPO fully approved in the external ERP.
REJECTEDPO rejected in the external ERP. The associated PR is automatically reopened.
CANCELLEDPurchase dropped. The PO is cancelled and so is the associated PR - nothing reopens.
Additional statuses such as RECEIVED (goods received), INVOICED (invoiced), and others will be added soon to reflect the full purchase order lifecycle.

Reference codes

Catalogs of currencies and payment terms accepted by the API. Use them to map these values to the equivalent codes in your ERP.

Currencies

Sourced does not use a closed currency enum: any valid 3-letter ISO 4217 code is accepted. The most commonly used currencies by our customers are:

CodeCurrency
ARSArgentine peso
USDUS dollar
EUREuro
BRLBrazilian real
Pass the ISO 4217 code directly in the currency field (e.g. "currency": "EUR"). Sourced stores it as-is and returns it unchanged in API responses.

Payment terms (payment_terms_code)

Global catalog of payment conditions available in Sourced. Use the code (Code column) in the PO's payment_terms_code field. The type field describes the nature of the payment: IMMEDIATE (on receipt), ADVANCE (prepaid), NET_DAYS (X days from invoice).

CodeNameTypeDaysDescription
CODCash On DeliveryIMMEDIATE0Cash on delivery
IMMImmediate PaymentIMMEDIATE0Immediate / Cash payment
ADV100Advance PaymentADVANCE0100% advance payment
ADV50Advance 50%ADVANCE050% advance payment
NET7Net 7 DaysNET_DAYS7Payment due 7 days after invoice date
NET10Net 10 DaysNET_DAYS10Payment due 10 days after invoice date
NET15Net 15 DaysNET_DAYS15Payment due 15 days after invoice date
NET20Net 20 DaysNET_DAYS20Payment due 20 days after invoice date
NET21Net 21 DaysNET_DAYS21Payment due 21 days after invoice date
NET30Net 30 DaysNET_DAYS30Payment due 30 days after invoice date
EOM30End of Month + 30NET_DAYS30Payment due end of month plus 30 days
NET35Net 35 DaysNET_DAYS35Payment due 35 days after invoice date
NET40Net 40 DaysNET_DAYS40Payment due 40 days after invoice date
NET45Net 45 DaysNET_DAYS45Payment due 45 days after invoice date
NET60Net 60 DaysNET_DAYS60Payment due 60 days after invoice date
NET75Net 75 DaysNET_DAYS75Payment due 75 days after invoice date
NET90Net 90 DaysNET_DAYS90Payment due 90 days after invoice date
The catalog is global to the entire platform and rarely changes. If you need an additional code for your integration, contact us.

Units of Measure

Unit of measure codes accepted in requisition items. Sent in the unit_of_measure field.

CodeUnit
EAEach / Unit
PCSPieces
KGKilogram
GGram
LBPound
OZOunce
MMeter
CMCentimeter
MMMillimeter
INInch
FTFoot
YDYard
LLiter
MLMilliliter
GALGallon
QTQuart
PTPint
FL_OZFluid ounce
M2Square meter
CM2Square centimeter
FT2Square foot
IN2Square inch
YD2Square yard
M3Cubic meter
CM3Cubic centimeter
FT3Cubic foot
IN3Cubic inch
YD3Cubic yard
BOXBox
CASECase
PACKPack
SETSet
KITKit
BUNDLEBundle
ROLLRoll
SHEETSheet
PALLETPallet
DRUMDrum
BAGBag
BOTTLEBottle
CENHundred
If you send an unrecognized unit, it is stored as-is. We recommend using standard codes so the AI can properly normalize supplier quotations.

Integration Flow

Typical integration pattern for a scheduled ERP sync (e.g., cron job every 15 minutes):

1

Collect new requisitions from ERP

Query your ERP for approved requisitions that haven't been synced to Sourced yet.

2

Check existing (optional)

Call POST /requisitions/check-existing with external_ids to filter out already-synced requisitions.

3

Import requisitions

Call POST /requisitions with the batch of new/updated requisitions. Store the returned requisition_ids.

4

Poll new purchase orders

Call GET /purchase-orders?since={last_sync_timestamp}&status=DRAFT to fetch new POs. Create the PO in your ERP and call POST /purchase-orders/{id}/status with {"status": "CREATED"}.

5

Confirm approved POs

When the PO is approved in your ERP, call POST /purchase-orders/{id}/status with {"status": "CONFIRMED"} to close the loop.

Pseudocode - Sync cron job
# Step 1: Get pending requisitions from ERP
ERP_REQS=$(query_erp_pending_requisitions)

# Step 2: Check which ones already exist in Sourced
EXISTING=$(curl -s -X POST \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d "{"external_ids": $ERP_REQ_IDS}" \
  https://api.gosourced.ai/api/v1/requisitions/check-existing)

# Step 3: Import only new requisitions
NEW_REQS=$(filter_not_found $ERP_REQS $EXISTING)
curl -s -X POST \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d "{"requisitions": $NEW_REQS}" \
  https://api.gosourced.ai/api/v1/requisitions

# Step 4: Pull new POs since last sync
POS=$(curl -s \
  -H "X-API-Key: $API_KEY" \
  "https://api.gosourced.ai/api/v1/purchase-orders?since=$LAST_SYNC&status=DRAFT")

# Step 5: Create POs in ERP and update status
for PO in $POS; do
  create_po_in_erp $PO
  curl -s -X POST \
    -H "X-API-Key: $API_KEY" \
    -H "Content-Type: application/json" \
    -d "{"status": "CREATED", "external_id": "$ERP_PO_NUMBER"}" \
    "https://api.gosourced.ai/api/v1/purchase-orders/$PO_ID/status"
done

# Step 6: When PO is approved in ERP, confirm it
curl -s -X POST \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status": "CONFIRMED"}' \
  "https://api.gosourced.ai/api/v1/purchase-orders/$PO_ID/status"

Full Example

A realistic import request with all available fields populated:

Complete import request
curl -X POST https://api.gosourced.ai/api/v1/requisitions \
  -H "X-API-Key: sk_live_abc123..." \
  -H "Content-Type: application/json" \
  -d '{
    "requisitions": [
      {
        "external_id": "CAL-REQ-2026-0042",
        "description": "Repuestos bomba centrífuga Planta Norte",
        "requester_name": "Carlos Rodríguez",
        "requester_email": "carlos.rodriguez@empresa.com",
        "comments": "Urgente: bomba fuera de servicio desde 03/03. <b>Necesitamos entrega express.</b>",
        "delivery_address": "Av. Industrial 4500, Parque Industrial Pilar, Buenos Aires",
        "delivery_address_code": "PLANTA-NORTE",
        "desired_delivery_lead_time_days": 7,
        "created_date": "2026-03-04T09:15:00Z",
        "offer_deadline": "2026-03-10T18:00:00Z",
        "total_estimated_value": 1250000.00,
        "currency": "ARS",
        "department_code": "MANT-001",
        "priority_level": "HIGH",
        "items": [
          {
            "external_line_id": "CAL-REQ-2026-0042-L10",
            "description": "Sello mecánico para bomba centrífuga KSB ETA 50-200",
            "quantity": 2,
            "unit_of_measure": "UN",
            "target_price": 185000.00,
            "estimated_price": 370000.00,
            "currency": "ARS",
            "category": "Repuestos Bombas",
            "material_code": "MAT-BOM-0234",
            "desired_delivery_lead_time_days": 10,
            "detail": "Sello tipo cartucho, material: carburo de silicio / carburo de silicio",
            "specifications": {
              "manufacturer_code": "KSB-SEAL-50200",
              "manufacturer_name": "KSB",
              "manufacturer_description": "Mechanical seal for ETA 50-200 centrifugal pump",
              "buyer_code": "MAT-BOM-0234",
              "buyer_code_description": "Sello mecánico bomba KSB ETA 50-200",
              "buyer_code_system": "Calipso",
              "technical_specs": "Diámetro eje: 35mm, Material caras: SiC/SiC, Elastómeros: Viton",
              "requirements": "Debe incluir certificado de calidad. Preferencia por repuesto original KSB."
            }
          },
          {
            "external_line_id": "CAL-REQ-2026-0042-L20",
            "description": "Rodamiento SKF 6310-2RS",
            "quantity": 4,
            "unit_of_measure": "UN",
            "target_price": 45000.00,
            "currency": "ARS",
            "category": "Rodamientos",
            "material_code": "MAT-ROD-0089",
            "specifications": {
              "manufacturer_code": "6310-2RS1",
              "manufacturer_name": "SKF",
              "technical_specs": "50x110x27mm, sellado ambos lados, grasa estándar"
            }
          },
          {
            "external_line_id": "CAL-REQ-2026-0042-L30",
            "description": "Aceite lubricante ISO VG 68",
            "quantity": 20,
            "unit_of_measure": "LT",
            "target_price": 5500.00,
            "currency": "ARS",
            "category": "Lubricantes",
            "specifications": {
              "technical_specs": "Aceite mineral ISO VG 68, índice de viscosidad > 95"
            }
          }
        ],
        "attachments": [
          {
            "url": "https://erp.empresa.com/files/plano-bomba-eta-50-200.pdf",
            "filename": "plano_bomba_KSB_ETA_50-200.pdf",
            "description": "Plano de despiece bomba KSB ETA 50-200",
            "file_type": "application/pdf"
          }
        ],
        "raw_data": {
          "calipso_doc_type": "SOL",
          "calipso_doc_number": "0042",
          "calipso_branch": "001",
          "approved_by": "María González",
          "cost_center": "CC-MANT-NORTE"
        }
      }
    ]
  }'
Response - 200 OK
{
  "success": true,
  "created_count": 1,
  "updated_count": 0,
  "skipped_count": 0,
  "error_count": 0,
  "errors": [],
  "requisition_ids": [1234]
}