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

# Purchase-to-Pay Flow

> Create a purchase order, receive goods, enter a vendor bill, and pay it end-to-end via the API.

## What this guide covers

The purchase-to-pay (P2P) workflow moves goods and money through four stages:

1. **Purchase order** -- commit to buying from a vendor
2. **Goods receipt** -- receive the goods into inventory
3. **Vendor bill** -- record the vendor's invoice and clear the GRNI accrual
4. **AP payment** -- pay the bill and close the liability

Each stage is a separate API call. GL entries post automatically at the right stage. No manual journal entries are needed for the standard flow.

## Prerequisites

* API key with scopes: `purchasing:write`, `purchasing:read`, `accounting:write`
* An active vendor account (account\_type: `vendor`)
* One or more product IDs to order
* A warehouse location ID

## Stage 1: Create the purchase order

`POST /v1/orders` with `document_type: "purchase_order"`. The order is created in `draft` status. No GL entry fires at this stage.

<CodeGroup>
  ```bash curl theme={null}
  curl -s -X POST "https://api.arcuserp.com/v1/orders" \
    -H "Authorization: Bearer $ARCUS_API_KEY" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: po-acme-2026-05-12-001" \
    -d '{
      "document_type": "purchase_order",
      "account_id": "acct_acme_vendor_uuid",
      "location_id": "loc_main_warehouse_uuid",
      "payment_terms": "Net30",
      "line_items": [
        {
          "product_id": "prod_dome_riser_uuid",
          "quantity": 24,
          "cost_rate": 18.00,
          "notes": "Dome Riser Kit - Q2 restock"
        },
        {
          "product_id": "prod_flat_riser_uuid",
          "quantity": 12,
          "cost_rate": 15.00
        }
      ]
    }'
  ```

  ```typescript TypeScript SDK theme={null}
  import Arcus from "@arcuserp/sdk";

  const arcus = new Arcus({ apiKey: process.env.ARCUS_API_KEY });

  const po = await arcus.orders.create({
    document_type: "purchase_order",
    account_id: "acct_acme_vendor_uuid",
    location_id: "loc_main_warehouse_uuid",
    payment_terms: "Net30",
    line_items: [
      {
        product_id: "prod_dome_riser_uuid",
        quantity: 24,
        cost_rate: 18.00,
        notes: "Dome Riser Kit - Q2 restock",
      },
      {
        product_id: "prod_flat_riser_uuid",
        quantity: 12,
        cost_rate: 15.00,
      },
    ],
  }, { headers: { "Idempotency-Key": "po-acme-2026-05-12-001" } });

  console.log(po.data.order_number); // PO-00042
  console.log(po.data.status);       // draft
  ```

  ```python Python SDK theme={null}
  import arcuserp, os

  client = arcuserp.Arcus(api_key=os.environ["ARCUS_API_KEY"])

  po = client.orders.create(
      document_type="purchase_order",
      account_id="acct_acme_vendor_uuid",
      location_id="loc_main_warehouse_uuid",
      payment_terms="Net30",
      line_items=[
          {
              "product_id": "prod_dome_riser_uuid",
              "quantity": 24,
              "cost_rate": 18.00,
              "notes": "Dome Riser Kit - Q2 restock",
          },
          {
              "product_id": "prod_flat_riser_uuid",
              "quantity": 12,
              "cost_rate": 15.00,
          },
      ],
      extra_headers={"Idempotency-Key": "po-acme-2026-05-12-001"},
  )

  print(po.data.order_number)  # PO-00042
  print(po.data.status)        # draft
  ```
</CodeGroup>

Save `po.data.id` (the PO UUID). You will reference it in later stages.

### Confirm the PO (open it)

Once reviewed, transition to `open` status, which makes it eligible for receiving:

```bash theme={null}
curl -s -X PATCH "https://api.arcuserp.com/v1/orders/$PO_ID" \
  -H "Authorization: Bearer $ARCUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status": "open"}'
```

***

## Stage 2: Receive goods

`POST /v1/orders/{po_id}/receive` records the goods arriving at your warehouse.

**GL entries that fire at this stage:**

| Account                            | Debit                      | Credit                     |
| ---------------------------------- | -------------------------- | -------------------------- |
| Inventory (Finished Goods)         | unit\_cost x qty\_received |                            |
| GRNI (Goods Received Not Invoiced) |                            | unit\_cost x qty\_received |

GRNI is a liability accrual that stays open until you post the vendor bill in Stage 3.

<CodeGroup>
  ```bash curl theme={null}
  curl -s -X POST "https://api.arcuserp.com/v1/orders/$PO_ID/receive" \
    -H "Authorization: Bearer $ARCUS_API_KEY" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: recv-po-00042-2026-05-14" \
    -d '{
      "received_date": "2026-05-14",
      "location_id": "loc_main_warehouse_uuid",
      "line_items": [
        {
          "order_item_id": "oi_dome_riser_uuid",
          "quantity_received": 24,
          "bin_id": "bin_A1_uuid"
        },
        {
          "order_item_id": "oi_flat_riser_uuid",
          "quantity_received": 12,
          "bin_id": "bin_A2_uuid"
        }
      ]
    }'
  ```

  ```typescript TypeScript SDK theme={null}
  const receipt = await arcus.orders.receive(poId, {
    received_date: "2026-05-14",
    location_id: "loc_main_warehouse_uuid",
    line_items: [
      { order_item_id: "oi_dome_riser_uuid", quantity_received: 24, bin_id: "bin_A1_uuid" },
      { order_item_id: "oi_flat_riser_uuid", quantity_received: 12, bin_id: "bin_A2_uuid" },
    ],
  }, { headers: { "Idempotency-Key": "recv-po-00042-2026-05-14" } });

  console.log(receipt.data.id);                    // receipt UUID
  console.log(receipt.data.inventory_updated);     // true
  ```

  ```python Python SDK theme={null}
  receipt = client.orders.receive(
      po_id,
      received_date="2026-05-14",
      location_id="loc_main_warehouse_uuid",
      line_items=[
          {"order_item_id": "oi_dome_riser_uuid", "quantity_received": 24, "bin_id": "bin_A1_uuid"},
          {"order_item_id": "oi_flat_riser_uuid", "quantity_received": 12, "bin_id": "bin_A2_uuid"},
      ],
      extra_headers={"Idempotency-Key": "recv-po-00042-2026-05-14"},
  )

  print(receipt.data.id)
  ```
</CodeGroup>

The response includes `inventory_updated: true` and the GL journal entry ID for the GRNI posting. Inventory on-hand increases immediately.

**Partial receipt:** you can receive fewer than the ordered quantity. The remaining quantity stays open on the PO. Fire another receive call when the rest arrives.

***

## Stage 3: Enter and post the vendor bill

When the vendor's invoice arrives, create a vendor bill linked to the PO. Posting the bill clears the GRNI accrual and records the AP liability.

### Create the draft bill

```bash theme={null}
curl -s -X POST "https://api.arcuserp.com/v1/vendor-bills" \
  -H "Authorization: Bearer $ARCUS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: bill-acme-inv-20260514" \
  -d '{
    "vendor_id": "acct_acme_vendor_uuid",
    "po_id": "'"$PO_ID"'",
    "vendor_invoice_number": "INV-2026-0514",
    "bill_date": "2026-05-14",
    "due_date": "2026-06-13",
    "line_items": [
      {
        "po_item_id": "oi_dome_riser_uuid",
        "quantity": 24,
        "unit_cost": 18.00,
        "description": "Dome Riser Kit"
      },
      {
        "po_item_id": "oi_flat_riser_uuid",
        "quantity": 12,
        "unit_cost": 15.00,
        "description": "Flat Riser Kit"
      }
    ]
  }'
```

The bill is created in `draft` status. No GL entry yet.

### Post the bill

```bash theme={null}
curl -s -X POST "https://api.arcuserp.com/v1/vendor-bills/$BILL_ID/post" \
  -H "Authorization: Bearer $ARCUS_API_KEY"
```

**GL entries that fire when the bill is posted:**

| Account                            | Debit             | Credit            |
| ---------------------------------- | ----------------- | ----------------- |
| GRNI (Goods Received Not Invoiced) | total bill amount |                   |
| Accounts Payable                   |                   | total bill amount |

This clears the GRNI accrual from Stage 2. The AP liability is now open until Stage 4.

***

## Stage 4: Pay the vendor bill

`POST /v1/ap-payments` closes the AP liability and records the outgoing cash.

<CodeGroup>
  ```bash curl theme={null}
  curl -s -X POST "https://api.arcuserp.com/v1/ap-payments" \
    -H "Authorization: Bearer $ARCUS_API_KEY" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: ap-pay-bill-20260613" \
    -d '{
      "vendor_id": "acct_acme_vendor_uuid",
      "payment_date": "2026-06-13",
      "payment_method": "check",
      "bank_account_id": "bank_operating_uuid",
      "memo": "Acme Inv 2026-0514 - Dome/Flat Riser restock",
      "applications": [
        {
          "bill_id": "'"$BILL_ID"'",
          "amount": 612.00
        }
      ]
    }'
  ```

  ```typescript TypeScript SDK theme={null}
  const payment = await arcus.apPayments.create({
    vendor_id: "acct_acme_vendor_uuid",
    payment_date: "2026-06-13",
    payment_method: "check",
    bank_account_id: "bank_operating_uuid",
    memo: "Acme Inv 2026-0514 - Dome/Flat Riser restock",
    applications: [
      { bill_id: billId, amount: 612.00 },
    ],
  }, { headers: { "Idempotency-Key": "ap-pay-bill-20260613" } });

  console.log(payment.data.check_number); // CHK-00142 (if payment_method = "check")
  console.log(payment.data.status);       // posted
  ```

  ```python Python SDK theme={null}
  payment = client.ap_payments.create(
      vendor_id="acct_acme_vendor_uuid",
      payment_date="2026-06-13",
      payment_method="check",
      bank_account_id="bank_operating_uuid",
      memo="Acme Inv 2026-0514 - Dome/Flat Riser restock",
      applications=[
          {"bill_id": bill_id, "amount": 612.00},
      ],
      extra_headers={"Idempotency-Key": "ap-pay-bill-20260613"},
  )

  print(payment.data.check_number)  # CHK-00142
  ```
</CodeGroup>

**GL entries that fire at payment:**

| Account                   | Debit  | Credit |
| ------------------------- | ------ | ------ |
| Accounts Payable          | 612.00 |        |
| Bank (Operating Checking) |        | 612.00 |

The AP liability is cleared. The vendor bill transitions to `paid` status.

***

## GL summary: the full P2P transaction trail

| Stage | Event            | Debit            | Credit           |
| ----- | ---------------- | ---------------- | ---------------- |
| 2     | Receive goods    | Inventory FG     | GRNI             |
| 3     | Post vendor bill | GRNI             | Accounts Payable |
| 4     | Pay bill         | Accounts Payable | Bank             |

At Stage 4, GRNI nets to zero and Inventory FG reflects the real cost. This is the standard accrual accounting pattern (NetSuite, Acumatica, QuickBooks Enterprise all follow the same sequence).

***

## Partial payments and multi-bill payments

A single AP payment can apply to multiple bills:

```json theme={null}
{
  "applications": [
    { "bill_id": "bill_uuid_1", "amount": 432.00 },
    { "bill_id": "bill_uuid_2", "amount": 180.00 }
  ]
}
```

You can also pay a bill partially. The bill remains `open` with a reduced `balance_due` until the remaining balance is paid or written off.

***

## Vendor credits

To apply a vendor credit against a bill payment, include `vendor_credit_ids[]` in the payment body:

```json theme={null}
{
  "vendor_credit_ids": ["vc_uuid_1"],
  "applications": [
    { "bill_id": "bill_uuid", "amount": 462.00 }
  ]
}
```

The credit reduces the cash portion of the payment. If credits fully cover the bill, no bank JE posts.

***

## Common errors

| Code                        | HTTP | Meaning                                                        |
| --------------------------- | ---- | -------------------------------------------------------------- |
| `po_not_found`              | 404  | PO UUID does not exist for this entity                         |
| `po_not_receivable`         | 422  | PO status is not `open` or `processing`                        |
| `quantity_exceeds_ordered`  | 422  | Receipt line quantity exceeds PO line quantity remaining       |
| `bill_already_posted`       | 409  | Attempted to post a bill that is already in `open` status      |
| `bill_balance_insufficient` | 422  | Payment application amount exceeds bill balance due            |
| `bank_account_not_found`    | 404  | `bank_account_id` does not exist for this entity               |
| `insufficient_scope`        | 403  | API key missing `purchasing:write` or `accounting:write` scope |

***

## Idempotency

Every write in this flow accepts `Idempotency-Key`. Use a stable key tied to your source record (e.g. `po-<source-po-id>`, `recv-<source-receipt-id>`, `bill-<vendor-invoice-number>`). Keys expire after 24 hours.

***

## Verifying the full chain

After all four stages, run these checks:

```bash theme={null}
# Confirm PO is fulfilled
curl "https://api.arcuserp.com/v1/orders/$PO_ID" \
  -H "Authorization: Bearer $ARCUS_API_KEY"
# Expect: status = fulfilled or processing

# Confirm inventory increased
curl "https://api.arcuserp.com/v1/inventory?product_id=prod_dome_riser_uuid" \
  -H "Authorization: Bearer $ARCUS_API_KEY"
# Expect: on_hand increased by 24

# Confirm bill is paid
curl "https://api.arcuserp.com/v1/vendor-bills/$BILL_ID" \
  -H "Authorization: Bearer $ARCUS_API_KEY"
# Expect: status = paid, balance_due = 0

# Run AP aging to confirm no residual payable
curl "https://api.arcuserp.com/v1/reports/ap-aging" \
  -H "Authorization: Bearer $ARCUS_API_KEY"
```

***

## Next steps

* [Journal Entries and Period Close](/guides/journal-entries-and-period-close) -- record adjusting entries and close the accounting period
* [Creating Orders](/guides/creating-orders) -- build a sales order on the outbound side
* [API Reference: Purchase Orders](/api-reference/purchasing/create-a-purchase-order)
* [API Reference: Vendor Bills](/api-reference/purchasing/create-a-vendor-bill)
* [API Reference: AP Payments](/api-reference/purchasing/create-an-ap-payment)
