> ## Documentation Index
> Fetch the complete documentation index at: https://docs.upwell.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Purchase orders and vendor invoices via API

> Upload a purchase order or vendor invoice PDF, let Upwell parse it, then list, search, and update the resulting records.

# 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 always-current request and response schemas, see the [API Reference](/api-reference/introduction) entries under **Purchase Orders** and **Vendor Invoices**.

<Note>
  These are **not** freight carrier invoices (`carrier_invoices`) and **not** customer AR invoices (`invoices`). For those flows see [Submitting carrier invoices via API](/api-guides/carrier-invoice-submission) and [Managing customer invoices (AR)](/api-guides/customer-invoices).
</Note>

## Prerequisites

* An **API key** on every request (`Authorization: YOUR_API_KEY`, no `Bearer` prefix). See [Authentication](/authentication).
* A **PDF** of the purchase order or vendor invoice (standalone supporting-doc uploads are PDF-only, one document per upload).

## The flow

<Steps>
  <Step title="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.
  </Step>

  <Step title="PUT the PDF bytes">
    Upload the raw file to `uploadUrl`. `Content-Type` must match the `mimeType` you declared (`application/pdf`).
  </Step>

  <Step title="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.
  </Step>

  <Step title="Read, search, or update">
    Use the list / get / search / patch endpoints below once the record exists.
  </Step>
</Steps>

## Step 1 — Request a presigned URL

Standalone supporting-doc ingestion uses a dedicated `associationType` and **no** `associationId`:

| Document       | `associationType` | `documentType`   |
| -------------- | ----------------- | ---------------- |
| Purchase order | `PURCHASE_ORDER`  | `PURCHASE_ORDER` |
| Vendor invoice | `VENDOR_INVOICE`  | `VENDOR_INVOICE` |

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.

```bash theme={null}
curl -X POST https://api.upwell.com/api/rest/generate-upload-presigned-url \
  -H "Authorization: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "input": {
          "associationType": "PURCHASE_ORDER",
          "documentType": "PURCHASE_ORDER",
          "fileName": "po-88123.pdf",
          "mimeType": "application/pdf",
          "sourceSystem": "YOUR_TMS",
          "sourceSystemId": "PO-DOC-88123"
        }
      }'
# -> { "generateUploadPresignedUrl": {
#      "documentId": "doc_...", "uploadUrl": "https://..." } }
```

For a vendor invoice, swap both types inside `input` to `VENDOR_INVOICE`. Read `uploadUrl` and `documentId` from `generateUploadPresignedUrl` — not from the response root.

<Tip>
  See [Integration patterns](/integration-patterns#document-upload-two-step-presigned-url) for the presigned-upload header gotcha (pass an `ArrayBuffer`, not a typed `Blob`, from Node `fetch`).
</Tip>

## Step 2 — Upload the PDF

```bash theme={null}
curl -X PUT "$UPLOAD_URL" \
  -H "Content-Type: application/pdf" \
  --data-binary @po-88123.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

```bash theme={null}
# Purchase orders created from this document
curl https://api.upwell.com/api/rest/documents/doc_abc123/purchase-orders \
  -H "Authorization: YOUR_API_KEY"

# Vendor invoices created from this document
curl https://api.upwell.com/api/rest/documents/doc_abc123/vendor-invoices \
  -H "Authorization: YOUR_API_KEY"
```

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`).

<Warning>
  **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.
</Warning>

### Find by your source ids

If you stored `sourceSystem` + `sourceSystemId` at presign time, you can look up the parsed record without the document id:

```bash theme={null}
curl -X POST https://api.upwell.com/api/rest/purchase-orders/search \
  -H "Authorization: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "sourceSystem": "YOUR_TMS", "sourceSystemId": "PO-DOC-88123" }'
```

Use `POST /api/rest/vendor-invoices/search` for vendor invoices — same body shape.

## Step 4 — List, get, and update

| Action           | Purchase orders                                  | Vendor invoices                                  |
| ---------------- | ------------------------------------------------ | ------------------------------------------------ |
| List recent      | `GET /api/rest/purchase-orders`                  | `GET /api/rest/vendor-invoices`                  |
| Get one          | `GET /api/rest/purchase-orders/{id}`             | `GET /api/rest/vendor-invoices/{id}`             |
| Update header    | `PATCH /api/rest/purchase-orders/{id}`           | `PATCH /api/rest/vendor-invoices/{id}`           |
| Update line item | `PATCH /api/rest/purchase-order-line-items/{id}` | `PATCH /api/rest/vendor-invoice-line-items/{id}` |

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.

```bash theme={null}
curl -X PATCH https://api.upwell.com/api/rest/purchase-orders/po_abc123 \
  -H "Authorization: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "input": { "poNumber": "PO-88123-R1" } }'
```

<Warning>
  **Money is integer cents.** `$1,250.00` → `125000`. See [Integration patterns](/integration-patterns).
</Warning>

## Related

* [Integration patterns](/integration-patterns) — presigned upload, integer cents, `sourceSystem` pairs
* [How documents flow through Upwell](/concepts/document-lifecycle)
* API Reference: [Purchase Orders](/api-reference/purchase-orders/list-purchase-orders), [Vendor Invoices](/api-reference/vendor-invoices/list-vendor-invoices)
