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.
curl -H "X-API-Key: sk_live_abc123..." \
https://api.gosourced.ai/api/v1/requisitionsBase URL
| Environment | Base URL |
|---|---|
| Production | https://api.gosourced.ai |
| Development | https://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.
{
"detail": "Invalid or revoked API key."
}| Status | Meaning |
|---|---|
| 200 | Success |
| 400 | Bad Request - validation error or malformed body |
| 401 | Unauthorized - missing or invalid API key |
| 404 | Not Found - resource does not exist or is not accessible |
| 422 | Unprocessable Entity - request body failed schema validation |
| 429 | Too Many Requests - rate limit exceeded |
| 500 | Internal Server Error - unexpected error on our side |
Error Examples
{
"detail": "Invalid or revoked API key."
}{
"detail": [
{
"loc": ["body", "requisitions", 0, "items", 0, "quantity"],
"msg": "Input should be greater than 0",
"type": "greater_than"
}
]
}{
"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
/api/v1/healthVerify the API is reachable. No authentication required.
curl https://api.gosourced.ai/api/v1/health{
"status": "ok",
"api": "v1"
}Upload a File (Presigned)
/api/v1/files/presignAPI Key RequiredGet 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.
Two-step flow (per file)
- Call this endpoint with the filename to receive an upload_url and the form fields.
- 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
| Field | Type | Required | Description |
|---|---|---|---|
| filename | string | required | Original filename (e.g. 'SOL_106177_LINE_001_SEQ010_adjunto.docx'). Path components are stripped. |
| content_type | string | optional | MIME type. If omitted, any content type is accepted. |
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"
}'{
"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.
# 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 successImport Requisitions
/api/v1/requisitionsAPI Key RequiredImport 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
| Field | Type | Required | Description |
|---|---|---|---|
| external_id | string | required | Unique ID in your ERP (deduplication key). Max 250 chars. |
| items | array | required | Line items (1-200 items). See Item schema below. |
| requester_name | string | optional | Name of the person requesting. Max 200 chars. |
| requester_email | string | optional | Email of the requester. Max 200 chars. |
| assigned_buyer | string | optional | Buyer assigned in the source ERP (free text). Shown and filterable in triage. Max 200 chars. |
| description | string | optional | Requisition title or summary. Max 500 chars. |
| comments | string | optional | Additional instructions for buyers (HTML supported). Max 15,000 chars. |
| delivery_address | string | optional | Free-text delivery address. Max 500 chars. |
| delivery_address_code | string | optional | Code matching an address configured in Sourced. Max 50 chars. |
| desired_delivery_lead_time_days | integer | optional | Desired lead time in days from PO confirmation. Applied as default to all items. |
| created_date | datetime | optional | When 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_deadline | datetime | optional | Deadline for supplier quotations (ISO 8601). |
| total_estimated_value | float | optional | Total estimated value. Auto-calculated from items if omitted. |
| currency | string | optional | Currency code (e.g., "ARS", "USD", "PYG"). Defaults to your organization's local currency. Max 10 chars. |
| department_code | string | optional | Department/cost center code (matched against Sourced departments). Max 50 chars. |
| priority_level | string | optional | Priority: "LOW", "MEDIUM", "HIGH", or "CRITICAL". |
| attachments | array | optional | File attachments (max 20). See Attachment schema below. |
| raw_data | object | optional | Arbitrary JSON from your ERP, stored for traceability. |
Item Object
| Field | Type | Required | Description |
|---|---|---|---|
| description | string | required | Shared with supplierItem name or description. Max 1,000 chars. |
| quantity | float | required | Shared with supplierRequired quantity (must be > 0). |
| external_line_id | string | optional | Line ID in your ERP (e.g., "REQ-001-L10"). Returned in POs for traceability. Max 100 chars. |
| unit_of_measure | string | optional | Shared with supplierUOM code (e.g., "KG", "EA", "LT", "M", "UN"). Matched against your org catalog. Max 50 chars. |
| target_price | float | optional | Target/budget price per unit. |
| estimated_price | float | optional | Estimated total price for this line. |
| currency | string | optional | Currency for prices (e.g., "ARS", "USD"). Max 10 chars. |
| category | string | optional | Category from your ERP. Max 200 chars. |
| material_code | string | optional | Shared with supplierMaterial/part code in your ERP (e.g., SAP material code). Max 100 chars. |
| specifications | object | optional | Technical specifications. See Specifications schema below. |
| desired_delivery_date | datetime | optional | Desired delivery date for this item (ISO 8601, e.g., "2026-05-15T00:00:00Z"). |
| desired_delivery_lead_time_days | integer | optional | Desired lead time in days for this line item. Overrides the header-level value. |
| detail | string | optional | Shared 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_data | object | optional | Arbitrary JSON for this line item. |
Specifications Object
| Field | Type | Required | Description |
|---|---|---|---|
| manufacturer_code | string | optional | Shared with supplierManufacturer part number (e.g., "6ES7214-1AG40-0XB0"). Max 100 chars. |
| manufacturer_name | string | optional | Shared with supplierManufacturer name (e.g., "Siemens"). Max 200 chars. |
| manufacturer_description | string | optional | Manufacturer's description. Max 500 chars. |
| buyer_code | string | optional | Shared with supplierInternal code in your system (e.g., "MAT-001234"). Max 100 chars. |
| buyer_code_description | string | optional | Description for the internal code. Max 500 chars. |
| buyer_code_system | string | optional | Source system name (e.g., "SAP", "Calipso"). Max 50 chars. |
| technical_specs | string | optional | Shared 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. |
| requirements | string | optional | Additional requirements (e.g., "ISO 9001 certification required"). Max 2,000 chars. Internal: not included in the email to the supplier. |
Attachment Object
| Field | Type | Required | Description |
|---|---|---|---|
| url | string | required | Publicly accessible URL to download the file. Max 2,000 chars. |
| filename | string | required | Original filename (e.g., "plano_motor.pdf"). Max 255 chars. |
| description | string | optional | Description of the attachment. Max 500 chars. |
| file_type | string | optional | MIME 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
PENDINGis found, it is updated in-place (items are fully replaced). - Supersede: If an existing requisition with status
SUPERSEDEDis 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.
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
| Field | Type | Required | Description |
|---|---|---|---|
| success | boolean | required | true if error_count is 0. |
| created_count | integer | required | Number of new requisitions created. |
| updated_count | integer | required | Number of existing PENDING requisitions updated. |
| skipped_count | integer | required | Number of skipped requisitions (currently always 0). |
| error_count | integer | required | Number of requisitions that failed to import. |
| errors | array | required | Array of {index, external_id, error} for each failed requisition. |
| requisition_ids | array | required | Internal Sourced IDs of created/updated requisitions. |
{
"success": true,
"created_count": 1,
"updated_count": 0,
"skipped_count": 0,
"error_count": 0,
"errors": [],
"requisition_ids": [1234]
}List Requisitions
/api/v1/requisitionsAPI Key RequiredRetrieve a paginated list of your imported requisitions. Returns only requisitions created via the API.
Query Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| page | integer | optional | Page number (default: 1). |
| page_size | integer | optional | Items per page, 1-100 (default: 20). |
| status | string | optional | Filter by status: "PENDING", "LAUNCHED", "DISCARDED", "SUPERSEDED". |
curl -H "X-API-Key: sk_live_abc123..." \
"https://api.gosourced.ai/api/v1/requisitions?page=1&page_size=20&status=PENDING"{
"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
/api/v1/requisitions/{requisition_id}API Key RequiredRetrieve a single requisition by its internal Sourced ID, including all items.
curl -H "X-API-Key: sk_live_abc123..." \
https://api.gosourced.ai/api/v1/requisitions/1234Returns the full requisition object. Returns 404 if not found or not owned by your organization.
Check Existing Requisitions
/api/v1/requisitions/check-existingAPI Key RequiredCheck which external_ids already exist in Sourced before importing. Useful to avoid unnecessary API calls.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| external_ids | array | required | List of external_id strings to check (1-100 items). |
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"]
}'{
"existing": ["REQ-2026-0042"],
"not_found": ["REQ-2026-0043", "REQ-2026-0044"]
}Cancel Requisition
/api/v1/requisitions/cancelAPI Key RequiredCancel 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).
| Field | Type | Required | Description |
|---|---|---|---|
| external_id | string | required | The requisition ID in your external system (the same one used when importing) |
| reason | string | optional | Cancellation reason (stored for audit purposes) |
| cancel_remaining | boolean | optional | Only 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.
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"
}'// 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 requisitionPartiallyAwardedwith awarded_lines. - The Purchase Request is being processed right now (for example, an award in progress) - returns
409 requisitionPrBusywith 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 requisitionBusywith 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
/api/v1/purchase-ordersAPI Key RequiredRetrieve purchase orders for your organization. Supports incremental sync via the 'since' parameter.
Query Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| page | integer | optional | Page number (default: 1). |
| page_size | integer | optional | Items per page, 1-100 (default: 20). |
| status | string | optional | Filter by status: "DRAFT", "CREATED", "SENT", "CONFIRMED", "REJECTED", "CANCELLED". |
| since | datetime | optional | ISO 8601 timestamp. Returns only POs created or updated after this date. |
| requisition_external_id | string | optional | Filter by the original requisition's external_id from your ERP. Returns POs generated from that requisition. |
curl -H "X-API-Key: sk_live_abc123..." \
"https://api.gosourced.ai/api/v1/purchase-orders?since=2026-03-01T00:00:00Z&status=CONFIRMED"{
"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
/api/v1/purchase-orders/{po_id}API Key RequiredRetrieve a single purchase order by its internal Sourced ID, including all line items with traceability back to your ERP via external_line_id.
curl -H "X-API-Key: sk_live_abc123..." \
https://api.gosourced.ai/api/v1/purchase-orders/567This 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:
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
| Field | Type | Required | Description |
|---|---|---|---|
| id | integer | required | Sourced internal PO ID. |
| code | string | required | Human-readable PO code. |
| status | string | required | PO status: "DRAFT", "CREATED", "SENT", "CONFIRMED", "REJECTED", "CANCELLED". |
| award_type | string | optional | Award type: FULL (single supplier) or PARTIAL (split award across multiple suppliers). |
| supplier_name | string | required | Supplier name. |
| supplier_id | integer | optional | Sourced internal supplier ID. |
| total_price | float | optional | Total PO value. |
| currency | string | optional | Currency code. |
| delivery_address | string | optional | Delivery address. |
| delivery_address_code | string | optional | Code 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. |
| observations | string | optional | Free-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_days | integer | optional | Header-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_date | datetime | optional | Computed 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_code | string | optional | Payment terms code. |
| awarded_at | datetime | optional | When the PO was awarded (ISO 8601). |
| awarded_by_name | string | optional | Name of the user who awarded this PO. |
| awarded_by_email | string | optional | Email of the user who awarded this PO. |
| created_at | datetime | optional | Creation timestamp (ISO 8601). |
| updated_at | datetime | optional | Last update timestamp (ISO 8601). |
| purchase_request_id | integer | optional | ID of the originating purchase request. |
| requisition_external_id | string | optional | Source-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_data | object | optional | Exchange rates at award time. Format: {"date": "2026-03-05", "usdToArs": 1450.0, "arsToUsd": 0.00069}. Null if no data available. |
| total_nominal_savings | float | optional | Total nominal savings vs historical prices (no inflation adjustment). |
| total_real_savings | float | optional | Total real savings vs historical prices (inflation adjusted). |
| savings_currency | string | optional | Currency of the savings amounts. |
| items | array | required | PO line items. See PO Item schema below. |
PO Item Object
| Field | Type | Required | Description |
|---|---|---|---|
| description | string | required | Item description. |
| quantity | float | optional | Quantity ordered. |
| unit_price | float | optional | Price per unit. |
| total_price | float | optional | Total line price (quantity x unit_price). |
| unit_of_measure | string | optional | Unit of measure. |
| currency | string | optional | Currency code. |
| delivery_lead_time_days | integer | optional | Delivery 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_date | datetime | optional | Computed delivery date for this line (PO.awarded_at + the line's delivery_lead_time_days). Null if either input is missing. |
| external_line_id | string | optional | Original line ID from your ERP - use this to match PO items back to your requisition lines. |
| buyer_code | string | optional | Internal code in your system, exactly as you sent it when importing the requisition (e.g. "MAT-001234"). |
| buyer_code_system | string | optional | Source system of the internal code (e.g. "SAP", "JDE", "Calipso"). |
| buyer_code_description | string | optional | Description of the internal code. |
| manufacturer_code | string | optional | Manufacturer part number. |
| manufacturer_name | string | optional | Manufacturer name (e.g. "Siemens"). |
| manufacturer_description | string | optional | Manufacturer's description of the part. |
| technical_specs | string | optional | Free-text technical specifications (e.g. "220V, 50Hz, IP55"). |
| requirements | string | optional | Additional requirements (e.g. "ISO 9001 certification required"). |
- Lead time exists at two levels:
delivery_lead_time_daysat the header (the supplier's headline lead time) anditems[].delivery_lead_time_daysper line item (the precise per-item value). When the supplier quotes different lead times per line, prefer the per-line value. expected_delivery_dateis a computed field:awarded_at + delivery_lead_time_days. Sourced computes it; the supplier does not send a date directly.- If
delivery_lead_time_daysis null,expected_delivery_datewill be null too. This typically means the supplier did not include a lead time in their quote.
Download PO Legajo (Audit Dossier)
/api/v1/purchase-orders/{po_id}/legajoAPI Key RequiredGet 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).
curl -H "X-API-Key: sk_live_abc123..." \
https://api.gosourced.ai/api/v1/purchase-orders/567/legajoResponse object
| Field | Type | Required | Description |
|---|---|---|---|
| download_url | string | required | Temporary presigned URL to download the legajo ZIP. |
| filename | string | required | Suggested filename for the ZIP. |
| expires_in | integer | required | Seconds until the download URL expires. |
| size_bytes | integer | required | Size of the legajo ZIP in bytes. |
- 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
/api/v1/purchase-orders/{po_id}/quotationsAPI Key RequiredGet 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.
curl -H "X-API-Key: sk_live_abc123..." \
https://api.gosourced.ai/api/v1/purchase-orders/567/quotations{
"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
| Field | Type | Required | Description |
|---|---|---|---|
| purchase_order_id | integer | required | ID of the queried PO. |
| purchase_order_code | string | required | Code of the queried PO (e.g. PO-4F2A91C3). |
| requisition_external_id | string | optional | External ID of the source requisition (ERP system), if any. |
| exchange_rate | object | optional | Exchange 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). |
| currencies | array | required | Distinct currencies appearing in the quotes, sorted (e.g. ["ARS", "USD"]). |
| items | array | required | Lines of the Purchase Request behind the PO, each with its quotes. |
Item Object (items[])
| Field | Type | Required | Description |
|---|---|---|---|
| material_code | string | optional | Buyer material code, if the line has one. |
| external_line_id | string | optional | Line ID from the source system (ERP), if any. |
| description | string | required | Description of the requested item. |
| quantity | float | optional | Requested quantity. |
| quotes | array | required | Quotes received for this line, one per supplier that quoted a price. |
Quote Object (items[].quotes[])
| Field | Type | Required | Description |
|---|---|---|---|
| quotation_id | integer | required | ID of the quotation. A supplier that quoted more than once (rounds) appears once per quotation. |
| supplier_id | integer | optional | Internal supplier ID in Sourced. Stable identity across lines (names may repeat). |
| supplier_name | string | optional | Name of the quoting supplier. |
| supplier_tax_id | string | optional | Supplier's tax ID (CUIT in Argentina, CNPJ in Brazil, RUT in Chile/Uruguay). |
| supplier_erp_code | string | optional | Supplier's ERP code in your organization, if configured. |
| unit_price | float | optional | FINAL 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_price | float | optional | Unit price before discount. Equals unit_price when there was no discount. |
| discount | object | optional | Applied 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. |
| currency | string | optional | Quote currency (ISO code, e.g. ARS, USD). When the supplier did not state a currency anywhere, "USD" is reported. |
| quantity | float | optional | Quantity quoted by the supplier. |
| awarded | boolean | required | true if this line was awarded to this supplier on any active PO of the request. |
| awarded_po_code | string | optional | Code of the PO where the line was awarded (may be a sibling PO in a split award). null if not awarded. |
| lead_time_days | integer | optional | Offered delivery lead time, in days (integer). A non-numeric lead time is reported as null. |
| payment_term_code | string | optional | Payment term code offered by the supplier. |
| response_status | string | optional | AI analysis classification of the supplier's response. Values such as INCOMPLETE, PARTIAL, NEEDS_HUMAN_REVIEW, EXPLICIT_REJECTION. |
| received_at | datetime | optional | Date and time the quote was received (ISO 8601). |
- 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.
awardedis request-level: in a split award, a line may have been awarded on a sibling PO different from the one queried -awarded_po_codealways 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_pricealready includes any negotiated discount (original_priceanddiscountshow 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_ratecarries 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
/api/v1/purchase-orders/{po_id}/statusAPI Key RequiredUpdate a purchase order's status to reflect its progress in your external system (ERP).
Allowed status transitions:
| From | To | Meaning |
|---|---|---|
| DRAFT | CREATED | The PO was created in your ERP. |
| CREATED | CONFIRMED | The PO was fully approved in your ERP. |
| DRAFT | CONFIRMED | Shortcut when no intermediate step is needed. |
| DRAFT | REJECTED | PO was rejected in your ERP. The PR is reopened in Sourced. |
| CREATED | REJECTED | PO was rejected in your ERP after being created. The PR is reopened in Sourced. |
| DRAFT / CREATED / SENT / CONFIRMED | CANCELLED | The 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.
| Field | Type | Required | Description |
|---|---|---|---|
| status | string | required | Target 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_id | string | optional | PO number or code in your ERP (e.g., OC-CAL-00045678). Stored for traceability. |
| notes | string | optional | Optional notes about the status change. |
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"
}'{
"status": "created",
"po_id": 567,
"code": "PO-A1B2C3D4"
}Edit a Purchase Order
/api/v1/purchase-orders/{po_id}API Key RequiredUpdate 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
| Field | Type | Required | Description |
|---|---|---|---|
| external_id | string | optional | PO number or code in your ERP. Overwrites the stored value. |
| payment_terms_code | string | optional | Payment 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_days | integer | optional | Delivery lead time in days from PO confirmation. expected_delivery_date derives from this value. |
| observations | string | optional | Free text shown on the PO. An empty string clears it. |
| status | string | optional | Target 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. |
| notes | string | optional | Notes about the status change. Only valid together with status. |
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"
}'{
"status": "updated",
"po_id": 567,
"code": "PO-A1B2C3D4",
"updated_fields": ["external_id", "payment_terms_code", "status"]
}- 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
/api/v1/suppliersAPI Key RequiredList suppliers for your organization. Use ?has_erp_code=false to find unmapped suppliers. Use ?detail=full for contacts, categories, and coverage.
Query Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| page | integer | optional | Page number (default: 1). |
| page_size | integer | optional | Results per page (1-200, default: 50). |
| search | string | optional | Search by name, custom name, email, tax ID, or ERP code. |
| erp_code | string | optional | Filter by exact ERP code. |
| has_erp_code | boolean | optional | true = only mapped to ERP, false = only unmapped. |
| detail | string | optional | Set to 'full' to include contacts, categories, and coverage. |
# 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"// 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
/api/v1/suppliersAPI Key RequiredCreate 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
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | required | Supplier name for your organization. |
| tax_id | string | optional | Tax ID (CUIT, CNPJ, RUT, etc.). At least one of tax_id or erp_code is required. |
| erp_code | string | optional | Supplier code in your ERP. At least one of tax_id or erp_code is required. |
| erp_type | string | optional | ERP type (SAP_B1, JDE, ORACLE_CLOUD, etc.). |
| country_code | string | optional | ISO 3166-1 country code (e.g., AR, BR, US). |
| city | string | optional | Supplier city. |
| address | string | optional | Full address. |
| state_code | string | optional | State/province (e.g., CABA, SP). |
| website | string | optional | Supplier website. |
| contacts | array | required | List of contacts. At least one with role 'sales' is required. |
| contacts[].name | string | required | Contact name. |
| contacts[].email | string | required | Contact email. |
| contacts[].phone | string | optional | Phone (optional). |
| contacts[].role | string | required | Role: SALES or LOGISTICS. |
| contacts[].is_primary | boolean | optional | true if primary contact (default: false). |
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"
}
]
}'// 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
/api/v1/delivery-trackingsAPI Key RequiredBulk 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).
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[])
| Field | Type | Required | Description |
|---|---|---|---|
| po_code | string | required | PO number in the customer's system. 100 characters max. |
| line_position | integer | required | Line position within the PO (10, 20, 30…). Integer ≥ 0. |
| original_delivery_date | date | required | Committed delivery date (YYYY-MM-DD) |
| supplier_erp_code | string | One of the two | Supplier code in the client's ERP. Recommended identifier. Required if supplier_tax_id is not sent. Max 100 characters. |
| supplier_tax_id | string | One of the two | Supplier tax ID (CUIT/RUT). Required if supplier_erp_code is not sent. Max 50 characters. |
| supplier_name | string | optional | Display-only text; never used for matching the supplier. 255 characters max. |
| material_code | string | optional | Material/item code in the customer's system. 100 characters max. |
| description | string | required | Item description. This is what the supplier sees in the follow-up email. Max 500 characters. |
| quantity_ordered | float | required | Ordered quantity. ≥ 0. This is what the supplier sees in the follow-up email. |
| quantity_pending | float | required | Quantity 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_measure | string | optional | Unit of measure. 20 characters max. |
| po_date | date | optional | PO date (YYYY-MM-DD). |
| buyer_email | string | optional | Escalation 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_urgent | boolean | optional | Marks 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.
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
| Field | Type | Required | Description |
|---|---|---|---|
| created_count | integer | required | Number of lines created. |
| updated_count | integer | required | Number of lines updated (includes reactivated ones). |
| unchanged_count | integer | required | Number of lines with no changes. |
| error_count | integer | required | Number of lines with an error. |
| results | array | required | Result for each line in the batch, in the same order they were sent. |
Per-line result object (results[])
| Field | Type | Required | Description |
|---|---|---|---|
| po_code | string | required | PO number, as sent. |
| line_position | integer | required | Line position, as sent. |
| id | integer | optional | Internal tracking id. Absent when the line errored. |
| result | string | required | created | updated | unchanged | reactivated | error. |
| status | string | optional | Tracking status after the upsert. Absent on error lines. |
| code | string | optional | Error code (dlv*). Only present when result is error. |
| message | string | optional | English error detail, meant for logs. Don't translate or show it to the end user: code is the i18n key. |
// 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
/api/v1/delivery-trackingsAPI Key RequiredGet your organization's delivery trackings, from every source (API, CSV and ERP sync). Supports incremental sync via since.
Query Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| page | integer | optional | Page number (default: 1). |
| page_size | integer | optional | Items per page, 1-200 (default: 50). |
| status | string | optional | Filter by status (see the status table below). Includes STAND_BY. |
| po_code | string | optional | Filter by exact po_code. |
| supplier_erp_code | string | optional | Filter by the supplier's ERP code. |
| since | datetime | optional | ISO 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_only | boolean | optional | Excludes DELIVERED and CANCELLED trackings. |
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
| Status | Description |
|---|---|
| PENDING_SCHEDULE | Pending schedule. Loaded; a confirmation has not been requested from the supplier yet. |
| READY_TO_SEND | Ready to send. Ready to send the delivery confirmation request. |
| SENT_PENDING_RESPONSE | Sent, awaiting response. Confirmation was requested; the supplier hasn't replied yet. |
| NO_RESPONSE | No response. No reply after the reminders. |
| CONFIRMED_ON_TIME | Confirmed on time. The supplier confirmed the original date. |
| CONFIRMED_DELAYED | Confirmed delayed. Confirmed, but with a date later than the original. |
| CONFIRMED_EARLY | Confirmed early. Confirmed a date earlier than the original. |
| REQUIRES_REVIEW | Requires review. The reply was ambiguous or changed conditions; needs a buyer's review. |
| DELIVERED | Delivered. Already delivered. |
| CANCELLED | Cancelled. The PO was cancelled. |
| SUPPLIER_NOT_FOUND | Supplier not identified. The supplier could not be matched (by ERP code or tax id). |
| STAND_BY | Manually 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. |
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
| Field | Type | Required | Description |
|---|---|---|---|
| id | integer | required | Internal tracking id. |
| po_code | string | required | PO number in the customer's system. |
| line_position | integer | required | Line position within the PO. |
| source | string | required | Where the tracking came from: api, csv or erp_sync. This endpoint reads trackings from all three sources. |
| supplier | object | optional | Linked supplier, or null if it could not be resolved yet (SUPPLIER_NOT_FOUND). |
| supplier.id | integer | optional | Internal supplier id. |
| supplier.name | string | optional | Supplier's legal name. |
| supplier.tax_id | string | optional | Supplier's tax id. |
| supplier.erp_code | string | optional | Supplier's ERP code for your organization. |
| supplier_name | string | optional | Supplier name as loaded (may differ from supplier.name if it only arrived as free text). |
| material_code | string | optional | Material/item code. |
| description | string | optional | Item description. |
| quantity_ordered | float | optional | Ordered quantity. |
| quantity_pending | float | optional | Quantity pending delivery. |
| unit_of_measure | string | optional | Unit of measure. |
| po_date | date | optional | PO date. |
| original_delivery_date | date | optional | Originally committed delivery date. |
| eta | date | optional | Estimated 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_date | date | optional | Actual delivery date, once status is DELIVERED. |
| received_quantity | float | optional | Quantity received, once status is DELIVERED. |
| status | string | optional | Public status (see the status table). |
| is_urgent | boolean | required | Whether the line is marked urgent. |
| paused | boolean | required | Whether follow-up sending is paused (sending_paused). |
| pause_reason | string | optional | Reason for the pause. null when not paused. |
| followup_count | integer | required | Number of reminders sent in the current cycle. |
| last_followup_sent_at | datetime | optional | Date and time of the last reminder sent. |
| next_followup_date | datetime | optional | Planned date for the next reminder. |
| supplier_response | object | required | The supplier's latest response. |
| supplier_response.responded_at | datetime | optional | Date and time the supplier responded. |
| supplier_response.confirmed_delivery_date | date | optional | Delivery date the supplier confirmed. |
| supplier_response.delay_reason | string | optional | Delay reason, if the supplier gave one. |
| supplier_response.notes | string | optional | Notes from the supplier's latest response. null if they haven't replied yet. |
| supplier_response.quality | string | optional | AI-assessed quality of the response (for example, concrete). |
| created_at | datetime | optional | When the tracking was created. |
| updated_at | datetime | optional | When it was last modified. |
{
"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
/api/v1/delivery-trackings/{id}API Key RequiredGet the detail of a tracking by its internal id.
Same shape as each element of GET /delivery-trackings.
curl -H "X-API-Key: sk_live_abc123..." \
https://api.gosourced.ai/api/v1/delivery-trackings/8821{
"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
/api/v1/delivery-trackings/{id}/eventsAPI Key RequiredPaginated timeline of a tracking's events, oldest first.
Query Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| page | integer | optional | Page number (default: 1). |
| page_size | integer | optional | Items per page, 1-200 (default: 50). |
Event object
| Field | Type | Required | Description |
|---|---|---|---|
| id | integer | required | Internal event id. |
| type | string | required | Event type (see the type table). |
| occurred_at | datetime | optional | When it happened. |
| actor_type | string | required | Who generated it: system, supplier, user or api. |
| data | object | required | Event detail; the shape depends on type (see the type table). |
Event types
| Type | data |
|---|---|
| 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? } |
{
"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
/api/v1/delivery-trackings/actionsAPI Key RequiredApply 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
| Field | Type | Required | Description |
|---|---|---|---|
| action | string | required | mark_delivered, cancel, pause, resume, set_urgent or unset_urgent. |
| targets | array | required | Lines to apply the action to. Between 1 and 200. |
| targets[].po_code | string | required | PO number. 100 characters max. |
| targets[].line_position | integer | optional | Line position. If omitted, applies to every open line of the PO. |
| reason | string | optional | Reason. Required for pause; optional otherwise. 500 characters max. |
| actual_delivery_date | date | optional | Actual delivery date. Only used with mark_delivered; defaults to today. |
| received_quantity | float | optional | Received quantity. Only used with mark_delivered. ≥ 0. |
Available actions
| action | Effect | Requires |
|---|---|---|
| mark_delivered | Moves to DELIVERED. actual_delivery_date uses the sent date or, if not sent, the organization's today. received_quantity is stored when sent. | — |
| cancel | Moves to CANCELLED. | — |
| pause | Pauses follow-up emails (sending_paused = true). | reason |
| resume | Resumes follow-up emails (sending_paused = false). | — |
| set_urgent | Marks the line as urgent. | — |
| unset_urgent | Unmarks 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.
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
| Field | Type | Required | Description |
|---|---|---|---|
| applied_count | integer | required | Number of trackings (not request targets) the action was applied to. already_* results don't count here. |
| error_count | integer | required | Number of trackings (not request targets) with an error. already_* results don't count here. |
| results | array | required | Result for each target. |
Per-target result object (results[])
| Field | Type | Required | Description |
|---|---|---|---|
| po_code | string | required | PO number. |
| line_position | integer | optional | Affected line position. May differ from the target if it was omitted (a whole PO can produce several results). |
| id | integer | optional | Internal tracking id. Absent when the target doesn't match any tracking. |
| result | string | required | applied, error, or one of the six idempotent results: already_delivered, already_cancelled, already_paused, already_active, already_urgent, already_not_urgent. |
| status | string | optional | Tracking status after the action. |
| code | string | optional | Error code (dlv*). Only present when result is error. |
| message | string | optional | English error detail, meant for logs. |
// 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
/api/v1/delivery-trackings/snapshotsAPI Key RequiredSend 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.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| lines | array | required | All 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
- Put into a single array all the lines you currently send in several batches of 200. Each line stays exactly as it is today.
- Send that array in a single POST to /delivery-trackings/snapshots and store the id from the response (202).
- Poll GET /delivery-trackings/snapshots/{id} every 10 to 15 seconds until status is COMPLETED, FAILED or SUPERSEDED.
- 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.
- Repeat it every time your process runs (once or twice a day). Lines that stop coming are closed automatically.
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
}
]
}'{
"id": 123,
"status": "PENDING",
"total_lines": 3120,
"received_at": "2026-09-28T13:05:00Z"
}Snapshot result
/api/v1/delivery-trackings/snapshots/{snapshot_id}API Key RequiredStatus and result of a snapshot. Poll it every few seconds until status is COMPLETED, FAILED or SUPERSEDED.
Snapshot object
| Field | Type | Required | Description |
|---|---|---|---|
| status | string | required | PENDING, PROCESSING, COMPLETED, FAILED or SUPERSEDED. |
| processed_lines | integer | required | Lines already applied: shows progress. |
| created_count / updated_count / unchanged_count / error_count | integer | required | Same counters as the upsert. |
| closed_count | integer | required | Lines closed because they weren't in the snapshot. |
| closure_skipped | boolean | required | true if the closure was skipped by the quantity safeguard. |
| closure_skipped_reason | string | optional | With closure_skipped = true, the reason: too_many_absent (the closure would have exceeded the quantity safeguard). Otherwise null. |
| would_close_count | integer | required | How many lines would have been closed. |
| superseded_by | integer | optional | With SUPERSEDED, the id of the newer snapshot that replaced it. |
| error_code | string | optional | With FAILED, dlvSnapshotProcessingFailed. A FAILED snapshot never closes lines: resending it is safe. |
| errors | array | required | Lines that couldn't be applied, with po_code, line_position, code and message. Those lines count as present: they aren't closed. |
curl https://api.gosourced.ai/api/v1/delivery-trackings/snapshots/123 \
-H "X-API-Key: sk_live_abc123..."{
"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.
| Code | HTTP | Meaning |
|---|---|---|
| dlvFeatureDisabled | 403 | Delivery tracking is not enabled for this organization. |
| dlvTrackingNotFound | 404 | Tracking not found (id doesn't exist, or belongs to another organization). |
| dlvTrackingManagedByOtherSource | 200 | This line is managed by another feed (CSV or ERP sync). |
| dlvSupplierReferenceConflict | 200 | The ERP code and tax id resolve to different suppliers. |
| dlvSupplierReferenceAmbiguous | 200 | The ERP code or tax ID matches more than one supplier. |
| dlvDuplicateLineInBatch | 200 | The line (same po_code and line_position) appears more than once in the batch. |
| dlvNothingPending | 200 | The line has nothing pending (quantity_pending = 0): no tracking is created to chase 0 units. |
| dlvConcurrentUpsert | 200 | The line was modified at the same time by another request. Retry. |
| dlvLineWriteFailed | 200 | The line could not be saved. Check the data and try again (not a concurrency issue). |
| dlvInvalidStatusFilter | 400 | The status value is not a valid status. |
| dlvInvalidSince | 400 | Invalid since date. Use ISO 8601 format. |
| dlvPauseReasonRequired | 400 | pause requires a reason. |
| dlvActionNotAllowedInStatus | 200 | The action does not apply to the tracking's current status. |
| dlvSnapshotDuplicateLines | 422 | The snapshot has the same line (po_code + line_position) more than once. params.keys lists the duplicates (up to 50). |
| dlvSnapshotNotFound | 404 | Snapshot not found (id doesn't exist, or belongs to another organization). |
| dlvSnapshotRateLimited | 429 | 6 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. |
| dlvSnapshotProcessingFailed | 200 | The 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). |
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.
Envelope
Every event arrives with this shape:
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
| X-Webhook-Signature | string | required | HMAC-SHA256 signature of the raw request body, prefixed with sha256=. |
| X-Webhook-Event | string | required | The 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-Id | string | required | The 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.
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)
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);
}
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.
Available events
| type | data | Fires when |
|---|---|---|
| purchase_order.created | the PO object | A new purchase order was created. |
| purchase_order.updated | id, code, status, reason | A purchase order's status or other data changed (for example, a rejection). |
| requisition.discarded | external_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_at | A requisition was discarded (for example, cancelled from the ERP with POST /requisitions/cancel). |
| delivery_tracking.response_received | tracking, event | The supplier replied to the delivery confirmation request. |
| delivery_tracking.status_changed | tracking, event | The tracking's status changed (includes reactivating a previously closed line, which goes back to PENDING_SCHEDULE). |
| delivery_tracking.delivered | tracking, event | The tracking moved to DELIVERED. |
| delivery_tracking.cancelled | tracking, event | The tracking moved to CANCELLED. |
Schema Reference
Quick reference of all request/response schemas used across endpoints.
Requisition Statuses
| Status | Description |
|---|---|
| PENDING | Imported, awaiting review by a buyer in Sourced. |
| LAUNCHED | Buyer has launched the requisition as a Purchase Request. |
| DISCARDED | Requisition was manually discarded. |
| SUPERSEDED | A newer version was imported with the same external_id. |
Purchase Order Statuses
| Status | Description |
|---|---|
| DRAFT | PO created in Sourced, pending sync to external ERP. |
| CREATED | PO created in the external ERP, pending approval. |
| CONFIRMED | PO fully approved in the external ERP. |
| REJECTED | PO rejected in the external ERP. The associated PR is automatically reopened. |
| CANCELLED | Purchase dropped. The PO is cancelled and so is the associated PR - nothing reopens. |
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:
| Code | Currency |
|---|---|
| ARS | Argentine peso |
| USD | US dollar |
| EUR | Euro |
| BRL | Brazilian real |
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).
| Code | Name | Type | Days | Description |
|---|---|---|---|---|
| COD | Cash On Delivery | IMMEDIATE | 0 | Cash on delivery |
| IMM | Immediate Payment | IMMEDIATE | 0 | Immediate / Cash payment |
| ADV100 | Advance Payment | ADVANCE | 0 | 100% advance payment |
| ADV50 | Advance 50% | ADVANCE | 0 | 50% advance payment |
| NET7 | Net 7 Days | NET_DAYS | 7 | Payment due 7 days after invoice date |
| NET10 | Net 10 Days | NET_DAYS | 10 | Payment due 10 days after invoice date |
| NET15 | Net 15 Days | NET_DAYS | 15 | Payment due 15 days after invoice date |
| NET20 | Net 20 Days | NET_DAYS | 20 | Payment due 20 days after invoice date |
| NET21 | Net 21 Days | NET_DAYS | 21 | Payment due 21 days after invoice date |
| NET30 | Net 30 Days | NET_DAYS | 30 | Payment due 30 days after invoice date |
| EOM30 | End of Month + 30 | NET_DAYS | 30 | Payment due end of month plus 30 days |
| NET35 | Net 35 Days | NET_DAYS | 35 | Payment due 35 days after invoice date |
| NET40 | Net 40 Days | NET_DAYS | 40 | Payment due 40 days after invoice date |
| NET45 | Net 45 Days | NET_DAYS | 45 | Payment due 45 days after invoice date |
| NET60 | Net 60 Days | NET_DAYS | 60 | Payment due 60 days after invoice date |
| NET75 | Net 75 Days | NET_DAYS | 75 | Payment due 75 days after invoice date |
| NET90 | Net 90 Days | NET_DAYS | 90 | Payment due 90 days after invoice date |
Units of Measure
Unit of measure codes accepted in requisition items. Sent in the unit_of_measure field.
| Code | Unit |
|---|---|
| EA | Each / Unit |
| PCS | Pieces |
| KG | Kilogram |
| G | Gram |
| LB | Pound |
| OZ | Ounce |
| M | Meter |
| CM | Centimeter |
| MM | Millimeter |
| IN | Inch |
| FT | Foot |
| YD | Yard |
| L | Liter |
| ML | Milliliter |
| GAL | Gallon |
| QT | Quart |
| PT | Pint |
| FL_OZ | Fluid ounce |
| M2 | Square meter |
| CM2 | Square centimeter |
| FT2 | Square foot |
| IN2 | Square inch |
| YD2 | Square yard |
| M3 | Cubic meter |
| CM3 | Cubic centimeter |
| FT3 | Cubic foot |
| IN3 | Cubic inch |
| YD3 | Cubic yard |
| BOX | Box |
| CASE | Case |
| PACK | Pack |
| SET | Set |
| KIT | Kit |
| BUNDLE | Bundle |
| ROLL | Roll |
| SHEET | Sheet |
| PALLET | Pallet |
| DRUM | Drum |
| BAG | Bag |
| BOTTLE | Bottle |
| CEN | Hundred |
Integration Flow
Typical integration pattern for a scheduled ERP sync (e.g., cron job every 15 minutes):
Collect new requisitions from ERP
Query your ERP for approved requisitions that haven't been synced to Sourced yet.
Check existing (optional)
Call POST /requisitions/check-existing with external_ids to filter out already-synced requisitions.
Import requisitions
Call POST /requisitions with the batch of new/updated requisitions. Store the returned requisition_ids.
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"}.
Confirm approved POs
When the PO is approved in your ERP, call POST /purchase-orders/{id}/status with {"status": "CONFIRMED"} to close the loop.
# 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:
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"
}
}
]
}'{
"success": true,
"created_count": 1,
"updated_count": 0,
"skipped_count": 0,
"error_count": 0,
"errors": [],
"requisition_ids": [1234]
}