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.

HOW IS v2 DIFFERENT FROM v1

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.

Three-level data structure

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

When a PO is raised or approved in your ERP — send the PO to SoftCo immediately, or as part of a scheduled sync, so SoftCo is ready to match when the supplier invoice arrives.
When a PO is amended or revised — resend the full updated PO. SoftCo will refresh the record using the same po_number.
During initial data migration — you can batch multiple POs in a single request to catch SoftCo up on open commitments from your ERP.
Resending is safe

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.

Quick Start — Single Purchase Order (v2)
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:

202 Accepted — you're done
{
  "status":      "SUCCESS",
  "total":       1,
  "succeeded":   1,
  "failed":      0,
  "errors":      [],
  "message":     "All records processed successfully."
}
What happens next

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.

Your ERP
HTTP POST
JSON
API Gateway
Auth & Routing
Validated
Integration
Layer
Validate & Transform
XML
Integration
Queue
Async Buffer
Delivered
SoftCo
AP
PO Available
1
Your ERP sends the POST request
After raising or approving a purchase order, your ERP sends a JSON payload to 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.
2
Gateway checks your credentials
Your API key is validated immediately by the API Gateway. An invalid or missing key returns 401 Unauthorized before your payload is processed.
3
Your payload is validated against the PO schema
The integration layer checks your JSON against the purchase order schema — the envelope structure, required header fields, the presence and structure of the 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.
4
Data is transformed and queued — you get 202
On success, the PO data is transformed into SoftCo's internal format and placed on the integration queue. You receive 202 Accepted immediately — no further action is needed from your side.
5
SoftCo receives the PO and makes it available for matching
The queue listener delivers the purchase order to SoftCo AP, where it is indexed by 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.
About the 202 Response

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

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

HeaderValueNotes
x-api-keyYour assigned API keyProvided at onboarding. Required on every request.
Client_IdOAuth 2.0 Client IDProvided at onboarding.
Client_SecretOAuth 2.0 Client SecretTreat as a password — do not share or log.
Access_Token_URLOAuth 2.0 token endpointProvided at onboarding.
Content-Typeapplication/JsonMust be present on every POST request.
Lost or compromised credentials?

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

CodeStatusMeaningWhat to do
202AcceptedPayload validated and queued. Delivery to SoftCo is in progress.Nothing — your purchase order is on its way.
207Multi-StatusBatch 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.
400Bad RequestPayload failed schema validation. A field-level error is in the response body.Read the message and errors fields, fix the payload, retry. See Troubleshooting.
401UnauthorizedAPI key missing or invalid.Check your x-api-key header value.
403ForbiddenValid API key but no permission for this endpoint.Contact your SoftCo Customer Success representative.
404Not FoundEndpoint path is incorrect.Verify the URL — check for typos or trailing slashes.
408Request TimeoutRequest took too long to process.Retry once after a short delay.
429Too Many RequestsRate limit exceeded.Slow your request rate and retry after the delay indicated.
500Internal Server ErrorUnexpected error on our side.Retry once. If it persists, raise a ticket with the requestId.
503Service UnavailableService temporarily unavailable.Retry after a short delay.

Endpoint

POST /purchase-orders/v2

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

HeaderValue
x-api-keyYour assigned API key.
Content-Typeapplication/Json
Client_IdYour OAuth 2.0 Client ID.
Client_SecretYour OAuth 2.0 Client Secret.
Access_Token_URLYour 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.

{
"objects": {
"object": {
"record": [ // array — one entry per PO
{
"po_number": "string", // required
"po_date": "YYYY-MM-DD", // recommended
"supplier_number": "string", // required
"currency": "EUR", // recommended
// ... other header fields ...
"rows": [ // array — one entry per order line
{
"line_number": 1,
"quantity": 10, // required
"unit": "BOX", // required
"unit_price": 100.00, // required
// ... other line fields ...
"coding": [ // array — one entry per coding split
{ "posting_line": 1, "distribution_percentage": 60, "gl_code": "6100", /* ... */ },
{ "posting_line": 2, "distribution_percentage": 40, "gl_code": "6200", /* ... */ }
] // sum of distribution_percentage must = 100
}
]
}
]
}
}
}

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.

Validation rule

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.

Benefit

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

FieldTypeStatusDescription
po_numberstringRequiredUnique purchase order number from your ERP. Used as the primary key for matching and updates.
supplier_numberstringRequiredYour supplier ID as it appears in SoftCo. Used to link the PO to the correct supplier record.
posting_datedateRecommendedThe accounting/posting date for this PO. Format: YYYY-MM-DD
po_datedateRequiredThe date the purchase order was raised. Format: YYYY-MM-DD
delivery_datedateRecommendedExpected delivery date. Format: YYYY-MM-DD
currencystringRecommended3-character ISO currency code. Example: EUR, GBP, USD
po_net_totalnumber (decimal)RecommendedNet total value of the purchase order.
po_gross_totalnumber (decimal)RecommendedGross total value including tax.
organizationstringRecommendedCompany/entity code. Defaults to "main" if omitted.
systemstringOptionalSource ERP system identifier. Example: NetSuite, SAP
po_typestringOptionalPO lifecycle status.
NEWUPDATECLOSE
descriptionstringOptionalFree-text description of the purchase order.
created_bystringOptionalName of the person who created the PO in the source ERP.
order_confirmationstringOptionalWhether order confirmation is required. Values: "true" or "false"
supplier_commentstringOptionalNotes or instructions for the supplier.

Supplier Fields

FieldTypeStatusDescription
supplier_namestringOptionalSupplier display name as it should appear on the PO.
supplier_address1stringOptionalSupplier address line 1 (typically street number).
supplier_address2stringOptionalSupplier address line 2 (street name, building).
supplier_citystringOptionalSupplier city.
supplier_postcodestringOptionalSupplier postal code.
supplier_statestringOptionalSupplier county, state, or region.
supplier_countrystringOptionalSupplier 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.

Delivery Address
FieldStatusDescription
delivery_address_descriptionOptionalLabel for the delivery location. Example: Warehouse - Building A
delivery_address1OptionalDelivery address line 1.
delivery_address2OptionalDelivery address line 2.
delivery_cityOptionalDelivery city.
delivery_postcodeOptionalDelivery postal code.
delivery_stateOptionalDelivery county or state.
delivery_countryOptionalDelivery country.
invoice_address_descriptionOptionalLabel for the invoice address. Example: Accounts Payable — Ireland
invoice_address1OptionalInvoice address line 1.
invoice_address2OptionalInvoice address line 2.
invoice_cityOptionalInvoice city.
invoice_postcodeOptionalInvoice postal code.
invoice_stateOptionalInvoice county or state.
invoice_countryOptionalInvoice 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.

FieldTypeDescription
label_1 … label_9stringCustom text labels. Use to pass ERP-specific classification or reference values.
date_1 / date_2 / date_3dateCustom date fields. Format: YYYY-MM-DD
amount_1 / amount_2 / amount_3number (decimal)Custom numeric amount fields.

Line Item Fields (rows[])

FieldTypeStatusDescription
line_numberintegerRequiredSequential line number. If omitted, SoftCo assigns one automatically.
quantitynumberRequiredOrdered quantity for this line.
unitstringRequiredUnit of measure. Examples: EA, BOX, KG, HR
unit_pricenumber (decimal)RequiredPrice per unit.
codingarrayRequiredAt least one coding row. See Coding Row Fields. All distribution_percentage values must sum to 100.
line_descriptionstringRecommendedHuman-readable description of what is being ordered on this line.
product_codestringRecommendedInternal product or item code from your ERP.
tax_codestringRecommendedTax code applicable to this line. Example: T1
row_net_totalnumber (decimal)RecommendedNet total for this line (quantity × unit_price).
product_namestringOptionalDisplay name of the product or service.
supplier_product_codestringOptionalSupplier's own product reference code.
unit_price_is_netstringRequiredWhether the unit price is net of tax. "true" or "false"
unit_price_is_fixedstringOptionalWhether the price is fixed and cannot be changed during matching. "true" or "false"
row_gross_totalnumber (decimal)OptionalGross total for this line including tax.
row_handling_stylestringRequiredControls how this line is matched during goods receipt.
GRN_LEVEL_MATCHHEADER_LEVEL_MATCHNO_MATCH
partial_invoice_indicatorstringRequiredHow partial invoice amounts are distributed across coding rows.
DISTRIBUTE_PROPORTIONALLYFIRST_LINELAST_LINE
quantity_is_unlimitedstringOptionalWhether quantity is open-ended (e.g. for blanket orders). "true" or "false"
tolerence_groupstringRequiredTolerance group name for invoice matching variance. Example: default. (Field name contains a typo — match exactly.)
difference_codestringOptionalCode controlling how price differences are handled. Example: ZeroPercentage
discount_diff_codestringOptionalCode controlling how discount differences are handled.
bypass_confirmationstringOptionalWhether to skip the order confirmation step. "true" or "false"

Coding Row Fields (rows[].coding[])

FieldTypeStatusDescription
distribution_percentagenumber (decimal)RequiredPercentage of the parent line value allocated to this coding row. All coding rows for a line must sum to exactly 100.
posting_lineintegerRequiredSequential number identifying this coding row within the parent line. Start at 1.
gl_codestringRequiredGL account code for this split. Should match a code in SoftCo's chart of accounts.
coding_net_amountnumber (decimal)RequiredNet amount attributed to this coding row (line net total × distribution_percentage / 100).
coding_tax_codestringRequiredTax code for this coding row. Usually matches the parent line's tax_code.
coding_gross_amountnumber (decimal)Required>Gross amount for this coding row.
quantitynumber (decimal)RequiredQuantity associated with this coding split (line quantity × distribution_percentage / 100).
coding_line_descriptionstringOptionalDescription specific to this coding split. Defaults to the parent line description if omitted.
posting_value_is_lockedstringOptionalWhether the coding amount is locked and cannot be changed during invoice processing. "true" or "false"
accounting_object_1–9stringOptionalAccounting 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_3dateOptionalCustom 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.

JSON — POST /purchase-orders/v2
{
  "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

CheckDetail
Envelope structureThe payload must use the PO envelope structure.
Required header fieldspo_number, po_date, and supplier_number must be present in every record.
rows array presenceEvery PO record must contain a rows array with at least one line item object.
coding array presenceEvery PO line must contain a coding array with at least one coding line object.
Required line fieldsEvery row must contain line_number, quantity, unit, unit_price, unit_price_is_net, row_handling_style, partial_invoice_indicatorand tolerence_group.
Required coding fieldsEvery coding row must contain posting_line, distribution_percentage, quantity, coding_net_amount, coding_gross_amount, coding_tax_code, and gl_code.
Field typesAll fields must be in line with the field type. Sending an incorrect data type for any field will fail validation.
Non-empty required valuesRequired 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:

400 Bad Request — validation failure
{
  "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

400
Coding percentages do not sum to 100
The distribution_percentage values across all coding rows for a line do not add up to exactly 100.
{ "status": "FAILED", "errors": [{"index": 0, "error": "coding distribution_percentage must sum to 100 for line 1"}] }
Fix: Check the coding rows for the failing line. Adjust percentages so they total 100 exactly.
400
Missing required field
A required field is absent. Common causes: missing po_number, supplier_number, quantity, unit, unit_price, or empty coding array.
{ "status": "FAILED", "errors": [{"index": 0, "error": "required key [quantity] not found"}] }
Fix: Ensure all required fields are present and non-null on every record and every line.
Resubmitting a PO

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

202 Accepted
{
  "status":      "SUCCESS",
  "total":       1,
  "succeeded":   1,
  "failed":      0,
  "errors":      [],
  "message":     "All records processed successfully."
}

Validation failure — 400 Bad Request

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

401 Unauthorized
{
  "status":   "FAILED",
  "message":  "Unauthorized"
}