Skip to main content

Purchase orders and vendor invoices via API

Purchase orders and vendor invoices are standalone supporting documents β€” inbound POs from your customers and generic (non-freight) vendor AP invoices. You upload a PDF through the same presigned-URL flow as other documents; Upwell parses it asynchronously and creates the structured record you can then list, search, and patch. This page covers the end-to-end workflow. For the complete request and response schemas, see the API Reference entries under Purchase Orders and Vendor Invoices.
These are not freight carrier invoices (carrier_invoices) and not customer AR invoices (invoices). For those flows see Submitting carrier invoices via API and Managing customer invoices (AR).

Prerequisites

  • An API key on every request (Authorization: YOUR_API_KEY β€” a Bearer prefix also works). See Authentication.
  • A PDF of the purchase order or vendor invoice. Standalone supporting-doc uploads are PDF-only, but a single upload can hold more than one document of the same type stacked together β€” Upwell detects the boundaries and splits it into separate records (see Combined-PDF uploads below).

The flow

1

Request a presigned upload URL

POST /api/rest/generate-upload-presigned-url with associationType of PURCHASE_ORDER or VENDOR_INVOICE. Omit associationId β€” the parsed record is created from the upload, not attached to an existing entity.
2

PUT the PDF bytes

Upload the raw file to uploadUrl. Content-Type must match the mimeType you declared (application/pdf).
3

Poll until the parsed record appears

GET /api/rest/documents/{documentId}/purchase-orders or .../vendor-invoices returns an empty list until parsing finishes (typically within a couple of minutes). Bound the poll β€” see Step 3.
4

Read, search, or update

Use the list / get / search / patch endpoints below once the record exists.

Step 1 β€” Request a presigned URL

Standalone supporting-doc ingestion uses a dedicated associationType and no associationId: Pass sourceSystem + sourceSystemId so retries are safe: re-requesting an already-uploaded document returns 400 DOCUMENT_ALREADY_UPLOADED; re-requesting one whose upload never completed returns a fresh uploadUrl for the same document.
For a vendor invoice, swap both types inside input to VENDOR_INVOICE. Read uploadUrl and documentId from generateUploadPresignedUrl β€” not from the response root.
The presigned URL is signed for host-only headers. You can send Content-Type on the PUT, but avoid adding headers that aren’t in X-Amz-SignedHeaders. Wrapping the body in a Blob with an explicit type can cause some Node fetch implementations to add headers that break the signature β€” passing an ArrayBuffer is the safe path. Full upload mechanics: Document conventions.

Step 2 β€” Upload the PDF

Uploaded bytes are verified server-side. Non-PDF content parks the document with status UPLOAD_CONTENT_REJECTED and is never parsed β€” check GET /api/rest/documents/{documentId}. Rejection is terminal for that sourceSystem + sourceSystemId; re-upload with a new sourceSystemId.

Step 3 β€” Poll for the parsed record

An empty list usually means parsing is still in progress β€” unless GET /api/rest/documents/{documentId} shows status: "UPLOAD_CONTENT_REJECTED" (invalid PDF; terminal for that sourceSystem + sourceSystemId).
Bound your poll. If the list is still empty after a few minutes and the document is not UPLOAD_CONTENT_REJECTED, stop and treat the upload as a failed parse. A valid PDF that is not recognized as a purchase order / vendor invoice can finish without creating a supporting record and without setting a rejection status β€” there is no separate mismatch flag on the document today. Re-upload with the correct document (or a new sourceSystemId) rather than polling forever.

Combined-PDF uploads

If your upload actually contains several purchase orders or vendor invoices stacked into one PDF, Upwell detects the boundaries and splits it into one child document per document found, rather than trying to parse the whole stack as a single record. The container document (the one you uploaded) moves through its own status sequence while this happens, which GET /api/rest/documents/{documentId} on the original documentId will show: You don’t need to look up the individual child documents yourself: GET /api/rest/documents/{documentId}/purchase-orders and .../vendor-invoices β€” the same polling endpoint from Step 3 β€” already match records from split children of that container as well as the container itself, so keep polling the original documentId you got from the presigned-URL response. Expect to see more than one record in the list once splitting has produced multiple documents; a PROCESSING_FAILED container may still have some purchase-order/vendor-invoice records from the children that succeeded before one of their siblings failed permanently.

Find by your source ids

If you stored sourceSystem + sourceSystemId at presign time, you can look up the parsed record without the document id:
Use POST /api/rest/vendor-invoices/search for vendor invoices β€” same body shape.

Step 4 β€” List, get, and update

List endpoints accept an optional limit query parameter. Get-one responses include line items and party addresses; vendor invoices also include the matched purchase order when one exists.

Patch body shape

Updates use { "input": { ...fields to set... } } with the record’s camelCase field names. Amounts are integer cents. System-managed columns (ids, tenant/document linkage, party references, match state) are not updatable and are rejected.
Money is integer cents. $1,250.00 β†’ 125000. See Integration patterns.