Purchase Orders v2
The unified purchase order format — with per-line coding rows for GL and cost-centre split distribution.
Overview
SoftCo supports two invoicing models. For non-PO invoices — expenses, ad-hoc services, or spend that does not require a formal commitment — invoices are coded and approved directly within SoftCo without any upstream purchase order. For PO-based invoices — where your organisation's procurement policy requires a purchase order before a supplier can invoice — SoftCo needs visibility of those POs from your ERP in order to validate and match against them. This API serves that second scenario.
The Purchase Orders API lets your ERP push purchase order data directly into SoftCo's AP platform, making each PO available for automated invoice matching. Once a PO is received, SoftCo can compare incoming supplier invoices against the committed quantities, unit prices, and GL coding on the order — reducing manual coding effort and accelerating invoice processing. Organisations using three-way matching can additionally pair this with the Goods Received API, requiring a confirmed receipt before an invoice can be approved.
The Purchase Orders v2 API is the unified PO format for SoftCo's AP platform. It extends the v1 format with coding rows — a nested structure within each order line that allows costs to be split across multiple GL accounts or cost centres before the invoice ever arrives. Where v1 carried a single GL code per line, v2 allows each line to have any number of coding rows, each with its own GL code, accounting objects, and distribution percentage.
A single call to POST /purchase-orders/v2 submits one or more complete purchase orders. Each order has a three-level structure: a PO header with order details and supplier information; one or more order lines in the rows array carrying pricing and quantities; and one or more coding rows in the coding array within each line, carrying the GL and cost-centre distribution for that line.
Where Purchase Orders fit in the AP workflow
SoftCo's AP platform is the system of record for invoice processing. When a supplier invoice arrives that references a purchase order, SoftCo looks up the PO your ERP has already sent via this API and compares the invoice against it — validating the supplier, quantities, and unit prices. Invoices that match within tolerance can be auto-approved; those outside tolerance are routed for review. For organisations that also use non-PO invoicing, both flows coexist: PO-referenced invoices are matched automatically, while non-PO invoices follow a direct coding and approval workflow. The richer the PO data you send, the more effectively SoftCo can automate the matching process.
Level 1 — Header fields: PO-level data (order number, dates, supplier, totals, addresses).
Level 2 — rows[]: One entry per order line (quantities, pricing, product codes, tax codes).
Level 3 — rows[].coding[]: One or more coding rows per line, carrying GL codes, accounting objects, and split percentages.
When to call this endpoint
po_number.
Resubmitting a PO that SoftCo has already received refreshes the record without creating a duplicate. This makes it straightforward to resend after a correction or to replay a batch.
Quick Start
The minimum viable v2 PO request — a single order with two line items, each with a single coding row. Copy this, swap in your credentials and real PO data, and send. The required fields at header level are po_number, supplier_number, and po_date. Each line in the rows array requires line_number, quantity, unit, unit_price, unit_price_is_net, row_handling_style, partial_invoice_indicator, and tolerence_group. Each coding row requires posting_line, distribution_percentage, quantity, coding_net_amount, coding_gross_amount, coding_tax_code, and gl_code.
POST /purchase-orders/v2 x-api-key: your-api-key-here Content-Type: application/Json Client_Id: your-client-id Client_Secret: your-client-secret { "objects": { "object": { "record": [ { "organization": "main", "po_number": "PO-2024-00891", "po_date": "2024-03-15", "supplier_number": "SUP-001234", "supplier_name": "Acme Office Supplies Ltd", "currency": "EUR", "po_net_total": 1250.00, "po_gross_total": 1537.50, "rows": [ { "line_number": 1, "line_description": "A4 Copy Paper — 5 Ream Box", "quantity": 10, "unit": "BOX", "unit_price": 62.50, "unit_price_is_net": "true", "row_net_total": 625.00, "row_handling_style": "GRN_LEVEL_MATCH", "partial_invoice_indicator": "DISTRIBUTE_PROPORTIONALLY", "tolerence_group": "default", "coding": [ { "posting_line": 1, "distribution_percentage": 100, "gl_code": "6100", "quantity": 10, "coding_net_amount": 625.00, "coding_gross_amount": 768.75, "coding_tax_code": "T1" } ] }, { "line_number": 2, "line_description": "Printer Ink Cartridge — Black", "quantity": 5, "unit": "EA", "unit_price": 125.00, "unit_price_is_net": "true", "row_net_total": 625.00, "row_handling_style": "GRN_LEVEL_MATCH", "partial_invoice_indicator": "DISTRIBUTE_PROPORTIONALLY", "tolerence_group": "default", "coding": [ { "posting_line": 1, "distribution_percentage": 100, "gl_code": "6100", "quantity": 5, "coding_net_amount": 625.00, "coding_gross_amount": 768.75, "coding_tax_code": "T1" } ] } ] } ] } } }
A successful response looks like this:
{ "status": "SUCCESS", "total": 1, "succeeded": 1, "failed": 0, "errors": [], "message": "All records processed successfully." }
A 202 Accepted means your purchase order has been validated and queued. The integration layer picks it up, transforms it into SoftCo's internal format, and delivers it for AP processing. The PO will be available for invoice matching once delivered, with coding rows pre-populating the GL distribution on matched invoices. If it does not appear within a few minutes, see the Troubleshooting section.
How It Works
Every request follows the same pipeline. Your system sends once — validation, transformation, and delivery to SoftCo are handled automatically.
Layer
Queue
AP
POST /purchase-orders/v2 with your credentials and one or more complete PO records — each containing a header, at least one line item in the rows array, and at least one coding row in each line's coding array.401 Unauthorized before your payload is processed.rows array, and the coding array within each line. Coding row distribution_percentage values must sum to exactly 100 per line. If anything fails, you get a 400 Bad Request with a field-level error message. Nothing is queued until validation passes.202 Accepted immediately — no further action is needed from your side.po_number and made available for invoice matching. Coding rows pre-populate the GL distribution on matched invoices. If SoftCo is temporarily unavailable, the queue holds the record and retries delivery automatically.202 Accepted does not mean the PO is available in SoftCo yet — it means your request has been received, validated, and queued successfully. Delivery to SoftCo AP is asynchronous and typically completes within minutes. Resending a PO is always safe; SoftCo will refresh the record using the po_number as the key. If a PO does not appear after a reasonable period, contact your SoftCo Customer Success representative.
Rate limits are configured per customer during onboarding. If you receive a 429 Too Many Requests response, reduce your request rate and retry after a short delay. For your specific rate limit thresholds, contact your SoftCo Customer Success representative.
Authentication
All requests require an API key and OAuth 2.0 credentials. Your credentials are provided during onboarding — keep them secure and never expose them in client-side code or version control.
Requests with a missing or invalid API key are rejected by the gateway immediately and never reach the integration layer.
Required Headers
| Header | Value | Notes |
|---|---|---|
| x-api-key | Your assigned API key | Provided at onboarding. Required on every request. |
| Client_Id | OAuth 2.0 Client ID | Provided at onboarding. |
| Client_Secret | OAuth 2.0 Client Secret | Treat as a password — do not share or log. |
| Access_Token_URL | OAuth 2.0 token endpoint | Provided at onboarding. |
| Content-Type | application/Json | Must be present on every POST request. |
Contact your SoftCo Customer Success representative immediately to rotate your API key. Do not attempt to use the same credentials across multiple customer environments.
Response Codes
| Code | Status | Meaning | What to do |
|---|---|---|---|
| 202 | Accepted | Payload validated and queued. Delivery to SoftCo is in progress. | Nothing — your purchase order is on its way. |
| 207 | Multi-Status | Batch partially accepted. Valid records were queued; one or more failed validation. Check the errors array for details on failing records. | Inspect the errors array by record index, correct the failing records, and resubmit only those that failed. |
| 400 | Bad Request | Payload failed schema validation. A field-level error is in the response body. | Read the message and errors fields, fix the payload, retry. See Troubleshooting. |
| 401 | Unauthorized | API key missing or invalid. | Check your x-api-key header value. |
| 403 | Forbidden | Valid API key but no permission for this endpoint. | Contact your SoftCo Customer Success representative. |
| 404 | Not Found | Endpoint path is incorrect. | Verify the URL — check for typos or trailing slashes. |
| 408 | Request Timeout | Request took too long to process. | Retry once after a short delay. |
| 429 | Too Many Requests | Rate limit exceeded. | Slow your request rate and retry after the delay indicated. |
| 500 | Internal Server Error | Unexpected error on our side. | Retry once. If it persists, raise a ticket with the requestId. |
| 503 | Service Unavailable | Service temporarily unavailable. | Retry after a short delay. |
Endpoint
Submit one or more purchase orders in the v2 unified format. Each record in the batch is validated independently — a single failing record does not block the rest.
Request Headers
| Header | Value |
|---|---|
| x-api-key | Your assigned API key. |
| Content-Type | application/Json |
| Client_Id | Your OAuth 2.0 Client ID. |
| Client_Secret | Your OAuth 2.0 Client Secret. |
| Access_Token_URL | Your OAuth 2.0 token endpoint URL. |
Request Body
The payload uses a fixed three-level JSON envelope. The record array can contain one PO or a batch of multiple POs.
Coding Rows
Coding rows are the key difference between v1 and v2. Each order line in v2 must include at least one coding row in its coding array. Coding rows carry the GL account code, accounting objects (cost centres, departments, projects), and the percentage of the line value that should be allocated to each account.
Simple case — no split (single coding row)
When the full value of a line is charged to a single GL account, include exactly one coding row with distribution_percentage: 100. The coding row amounts should match the parent line amounts.
Split case — multiple coding rows
When a line's cost is shared between departments or projects, include one coding row per split. Each row carries its own GL code, accounting objects, and the percentage it represents. The system validates that all distribution_percentage values for a given line sum to exactly 100.
All distribution_percentage values across the coding array for a single order line must sum to exactly 100. The API will return a 400 error for any line where coding percentages do not add up to 100. For example, two coding rows with distribution_percentage: 40 and distribution_percentage: 60 are valid (40 + 60 = 100).
Coding row amounts
Set coding_net_amount and coding_gross_amount to the portion of the line value that corresponds to this coding row's percentage. For a line with row_net_total: 1000 split 60/40, the first coding row should have coding_net_amount: 600 and the second coding_net_amount: 400.
When SoftCo matches a supplier invoice against this PO, the coding rows pre-populate the GL distribution on the invoice — eliminating the need for AP staff to manually code each line. This is particularly valuable for standing orders and recurring purchases where the cost split is known in advance.
Field Reference
PO Header Fields
| Field | Type | Status | Description |
|---|---|---|---|
| po_number | string | Required | Unique purchase order number from your ERP. Used as the primary key for matching and updates. |
| supplier_number | string | Required | Your supplier ID as it appears in SoftCo. Used to link the PO to the correct supplier record. |
| posting_date | date | Recommended | The accounting/posting date for this PO. Format: YYYY-MM-DD |
| po_date | date | Required | The date the purchase order was raised. Format: YYYY-MM-DD |
| delivery_date | date | Recommended | Expected delivery date. Format: YYYY-MM-DD |
| currency | string | Recommended | 3-character ISO currency code. Example: EUR, GBP, USD |
| po_net_total | number (decimal) | Recommended | Net total value of the purchase order. |
| po_gross_total | number (decimal) | Recommended | Gross total value including tax. |
| organization | string | Recommended | Company/entity code. Defaults to "main" if omitted. |
| system | string | Optional | Source ERP system identifier. Example: NetSuite, SAP |
| po_type | string | Optional | PO lifecycle status.
NEWUPDATECLOSE
|
| description | string | Optional | Free-text description of the purchase order. |
| created_by | string | Optional | Name of the person who created the PO in the source ERP. |
| order_confirmation | string | Optional | Whether order confirmation is required. Values: "true" or "false" |
| supplier_comment | string | Optional | Notes or instructions for the supplier. |
Supplier Fields
| Field | Type | Status | Description |
|---|---|---|---|
| supplier_name | string | Optional | Supplier display name as it should appear on the PO. |
| supplier_address1 | string | Optional | Supplier address line 1 (typically street number). |
| supplier_address2 | string | Optional | Supplier address line 2 (street name, building). |
| supplier_city | string | Optional | Supplier city. |
| supplier_postcode | string | Optional | Supplier postal code. |
| supplier_state | string | Optional | Supplier county, state, or region. |
| supplier_country | string | Optional | Supplier country. |
Address Fields
Both the delivery address and invoice address are optional. All address fields use the prefixes delivery_ and invoice_ respectively. Note that the description field for each address block uses the field name delivery_address_description and invoice_address_description.
| Field | Status | Description |
|---|---|---|
| delivery_address_description | Optional | Label for the delivery location. Example: Warehouse - Building A |
| delivery_address1 | Optional | Delivery address line 1. |
| delivery_address2 | Optional | Delivery address line 2. |
| delivery_city | Optional | Delivery city. |
| delivery_postcode | Optional | Delivery postal code. |
| delivery_state | Optional | Delivery county or state. |
| delivery_country | Optional | Delivery country. |
| invoice_address_description | Optional | Label for the invoice address. Example: Accounts Payable — Ireland |
| invoice_address1 | Optional | Invoice address line 1. |
| invoice_address2 | Optional | Invoice address line 2. |
| invoice_city | Optional | Invoice city. |
| invoice_postcode | Optional | Invoice postal code. |
| invoice_state | Optional | Invoice county or state. |
| invoice_country | Optional | Invoice country. |
Custom Header Fields
The v2 PO header supports custom label, date, and amount fields for ERP-specific data that does not map to a standard field.
| Field | Type | Description |
|---|---|---|
| label_1 … label_9 | string | Custom text labels. Use to pass ERP-specific classification or reference values. |
| date_1 / date_2 / date_3 | date | Custom date fields. Format: YYYY-MM-DD |
| amount_1 / amount_2 / amount_3 | number (decimal) | Custom numeric amount fields. |
Line Item Fields (rows[])
| Field | Type | Status | Description |
|---|---|---|---|
| line_number | integer | Required | Sequential line number. If omitted, SoftCo assigns one automatically. |
| quantity | number | Required | Ordered quantity for this line. |
| unit | string | Required | Unit of measure. Examples: EA, BOX, KG, HR |
| unit_price | number (decimal) | Required | Price per unit. |
| coding | array | Required | At least one coding row. See Coding Row Fields. All distribution_percentage values must sum to 100. |
| line_description | string | Recommended | Human-readable description of what is being ordered on this line. |
| product_code | string | Recommended | Internal product or item code from your ERP. |
| tax_code | string | Recommended | Tax code applicable to this line. Example: T1 |
| row_net_total | number (decimal) | Recommended | Net total for this line (quantity × unit_price). |
| product_name | string | Optional | Display name of the product or service. |
| supplier_product_code | string | Optional | Supplier's own product reference code. |
| unit_price_is_net | string | Required | Whether the unit price is net of tax. "true" or "false" |
| unit_price_is_fixed | string | Optional | Whether the price is fixed and cannot be changed during matching. "true" or "false" |
| row_gross_total | number (decimal) | Optional | Gross total for this line including tax. |
| row_handling_style | string | Required | Controls how this line is matched during goods receipt.
GRN_LEVEL_MATCHHEADER_LEVEL_MATCHNO_MATCH
|
| partial_invoice_indicator | string | Required | How partial invoice amounts are distributed across coding rows.
DISTRIBUTE_PROPORTIONALLYFIRST_LINELAST_LINE
|
| quantity_is_unlimited | string | Optional | Whether quantity is open-ended (e.g. for blanket orders). "true" or "false" |
| tolerence_group | string | Required | Tolerance group name for invoice matching variance. Example: default. (Field name contains a typo — match exactly.) |
| difference_code | string | Optional | Code controlling how price differences are handled. Example: ZeroPercentage |
| discount_diff_code | string | Optional | Code controlling how discount differences are handled. |
| bypass_confirmation | string | Optional | Whether to skip the order confirmation step. "true" or "false" |
Coding Row Fields (rows[].coding[])
| Field | Type | Status | Description |
|---|---|---|---|
| distribution_percentage | number (decimal) | Required | Percentage of the parent line value allocated to this coding row. All coding rows for a line must sum to exactly 100. |
| posting_line | integer | Required | Sequential number identifying this coding row within the parent line. Start at 1. |
| gl_code | string | Required | GL account code for this split. Should match a code in SoftCo's chart of accounts. |
| coding_net_amount | number (decimal) | Required | Net amount attributed to this coding row (line net total × distribution_percentage / 100). |
| coding_tax_code | string | Required | Tax code for this coding row. Usually matches the parent line's tax_code. |
| coding_gross_amount | number (decimal) | Required> | Gross amount for this coding row. |
| quantity | number (decimal) | Required | Quantity associated with this coding split (line quantity × distribution_percentage / 100). |
| coding_line_description | string | Optional | Description specific to this coding split. Defaults to the parent line description if omitted. |
| posting_value_is_locked | string | Optional | Whether the coding amount is locked and cannot be changed during invoice processing. "true" or "false" |
| accounting_object_1–9 | string | Optional | Accounting dimension values (cost centre, department, project, etc.) for this coding row. Each accounting_object_N field maps to a configured dimension in SoftCo. |
| date_1 / date_2 / date_3 | date | Optional | Custom date fields for this coding row. Format: YYYY-MM-DD |
Example Requests
Single PO with split coding
This example shows a two-line PO where line 1 is split 40%/60% across two GL accounts, and line 2 is fully charged to one account.
{ "objects": { "object": { "record": [{ "organization": "main", "system": "NetSuite", "po_number": "PO-2024-00891", "po_type": "NEW", "posting_date": "2024-03-15", "po_date": "2024-03-15", "delivery_date": "2024-03-18", "currency": "EUR", "description": "Office Supplies from Acme", "created_by": "Jane Murphy", "po_net_total": 1625.00, "po_gross_total": 1625.00, "supplier_number": "SUP-001234", "supplier_name": "Acme Office Supplies Ltd", "rows": [ { "line_number": 1, "line_description": "A4 Copy Paper — 5 Ream Box", "product_code": "72110", "quantity": 10, "unit": "BOX", "unit_price": 100.00, "unit_price_is_net": "true", "row_net_total": 1000.00, "tax_code": "T1", "row_gross_total": 1000.00, "coding": [ { "posting_line": 1, "distribution_percentage": 40, "gl_code": "6100", "coding_net_amount": 400.00, "coding_tax_code": "T1", "coding_gross_amount": 400.00 }, { "posting_line": 2, "distribution_percentage": 60, "gl_code": "6200", "coding_net_amount": 600.00, "coding_tax_code": "T1", "coding_gross_amount": 600.00 } ] }, { "line_number": 2, "line_description": "Printer Ink Cartridge — Black", "product_code": "72120", "quantity": 5, "unit": "EA", "unit_price": 125.00, "unit_price_is_net": "true", "row_net_total": 625.00, "tax_code": "T1", "row_gross_total": 625.00, "coding": [ { "posting_line": 1, "distribution_percentage": 100, "gl_code": "6100", "coding_net_amount": 625.00, "coding_tax_code": "T1", "coding_gross_amount": 625.00 } ] } ] }] }} }
Validation
Every request is validated against the purchase order schema before any processing takes place. If validation fails, the entire request is rejected immediately — nothing is queued or forwarded to SoftCo. You receive a clear error identifying the field or structural issue.
What is checked
| Check | Detail |
|---|---|
| Envelope structure | The payload must use the PO envelope structure. |
| Required header fields | po_number, po_date, and supplier_number must be present in every record. |
| rows array presence | Every PO record must contain a rows array with at least one line item object. |
| coding array presence | Every PO line must contain a coding array with at least one coding line object. |
| Required line fields | Every row must contain line_number, quantity, unit, unit_price, unit_price_is_net, row_handling_style, partial_invoice_indicatorand tolerence_group. |
| Required coding fields | Every coding row must contain posting_line, distribution_percentage, quantity, coding_net_amount, coding_gross_amount, coding_tax_code, and gl_code. |
| Field types | All fields must be in line with the field type. Sending an incorrect data type for any field will fail validation. |
| Non-empty required values | Required fields must not be empty strings. |
Validation error response
When validation fails, the API returns a 400 Bad Request with a JSON body identifying the failing field or structure:
{ "status": "FAILED", "total": 1, "succeeded": 0, "failed": 1, "errors": [{ "index": 0, "error": "required key [po_number] not found" }], "message": "1 of 1 records failed validation." }
The errors array identifies the exact field or path that failed, and the message field provides a summary. Fix the identified issue and resubmit.
Troubleshooting
distribution_percentage values across all coding rows for a line do not add up to exactly 100.po_number, supplier_number, quantity, unit, unit_price, or empty coding array.Sending a PO with the same po_number is always safe — SoftCo will update the existing record. No need to delete or close first.
Example Responses
Success — 202 Accepted
{ "status": "SUCCESS", "total": 1, "succeeded": 1, "failed": 0, "errors": [], "message": "All records processed successfully." }
Validation failure — 400 Bad Request
{ "status": "FAILED", "total": 1, "succeeded": 0, "failed": 1, "errors": [{ "index": 0, "error": "required key [supplier_number] not found" }], "message": "1 of 1 records failed validation." }
Authentication failure — 401 Unauthorized
{ "status": "FAILED", "message": "Unauthorized" }