# Change a purchase order's status

```http
PUT https://secure.yoprint.com/v1/api/store/{team_slug}/purchase_order/{hash_id}/status
```

- API version: v1
- Operation ID: `v1_public_purchase_order_change_status`
- Section: Purchasing › [Purchase orders](https://developer.yoprint.com/reference/purchase-orders.md)
- Web page: https://developer.yoprint.com/reference/purchase-orders/v1-purchase-order-change-status/
- Authentication: `Authorization: Bearer YOUR_API_KEY`

Moves a purchase order to another of the team's purchase order statuses and returns the purchase order, in the same shape as Get a purchase order.

This operation is kept for YoPrint's Zapier app, which uses this path, and its answer keeps the flat line fields Zapier reads: each line in `items` has `product` (`type`, with `id` or `name`), `quantity`, `price` and totals as plain values. The public API has no newer purchase order operations to use instead: creating, editing and receiving purchase orders are not part of it.

Moving it to the final status (`type` `end`) marks it complete. When it is delivered to a company location that holds stock, every line except fees must be fully received first: this operation does not receive anything. Any other status clears the completion date.

`{hash_id}` is the purchase order's `id`, not its number (`PO0042`).

## Permission

Who can update it: the team owner; a role whose Manage Purchase Order level includes `update_all_po`, for any of the team's purchase orders; or one with `update_self_po`, only for purchase orders the API key's user created or is assigned to (any other purchase order answers 404).

## Business rules

- `status_id` must be a purchase order status of this team (role `purchase_order`): a sales order, job or shipment status is refused.
- To move to the `end` status, a purchase order delivered to a company location that holds stock must have every live line except fees fully received (deleted and voided lines don't count), and that location must still be current (not deleted or replaced).
- Sending the status it already has is accepted: it saves again and sends the webhook again.

## Side effects

- Sets the status and its change date; an `end` status also sets `status_completion_date`, any other clears it.
- Sends the `purchase_order.status_changed` webhook on every call, even when the status didn't change.
- When the status really changes, adds a status change to the purchase order's history.
- Sends no email and does not sync to QuickBooks Online.

## Path parameters

- `team_slug` (string, required): Your team slug, shown as Team ID next to your API key in Profile. Example `your-team`.
- `hash_id` (string, required): The purchase order's id (not its number, such as `PO0042`). A hash id from an earlier answer; unique keys and document numbers (such as `PO0042`) don't work here. Example `EDhVUaC6jbLbed`.

## Request body

Content type `application/json`, required.

Fields:

- `status_id` (string, required): The `id` of one of the team's purchase order statuses, from List purchase order statuses.

### Example request

```bash
curl -X PUT 'https://secure.yoprint.com/v1/api/store/your-team/purchase_order/EDhVUaC6jbLbed/status' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Accept: application/json' \
  -H 'X-Yoprint-Decimal-Format: exact-v1' \
  -H 'Content-Type: application/json' \
  -d '{
    "status_id": "mOR2M727uLiHaX"
  }'
```

## Response

`200` Successful response.

Fields:

- `data` (object, required)
  - `calc_subtotal` (integer | string, required): The purchase order's subtotal. Thousandths, an older encoding: `10800` is 10.80. A whole number of thousandths comes back as a JSON integer; an amount finer than a thousandth comes back as a decimal string, such as `"12656.3"`, whatever headers you send. Parse it as a decimal. See [Money and quantities](/concepts/money-and-quantities/).
  - `calc_tax` (integer | string, required): Its tax. Thousandths, an older encoding: `10800` is 10.80. A whole number of thousandths comes back as a JSON integer; an amount finer than a thousandth comes back as a decimal string, such as `"12656.3"`, whatever headers you send. Parse it as a decimal. See [Money and quantities](/concepts/money-and-quantities/).
  - `calc_total` (integer | string, required): Its total. Thousandths, an older encoding: `10800` is 10.80. A whole number of thousandths comes back as a JSON integer; an amount finer than a thousandth comes back as a decimal string, such as `"12656.3"`, whatever headers you send. Parse it as a decimal. See [Money and quantities](/concepts/money-and-quantities/).
  - `id` (string, required): The purchase order's id: use it in the path.
  - `items` (array of objects, required): Every line of the purchase order, including deleted and voided ones: the rows don't say which.
    - Each item:
      - `calc_subtotal` (integer | string, required): The line's subtotal as YoPrint stores it: usually `price` times the quantity ordered; on some older lines, times the quantity still to receive. Thousandths, an older encoding: `10800` is 10.80. A whole number of thousandths comes back as a JSON integer; an amount finer than a thousandth comes back as a decimal string, such as `"12656.3"`, whatever headers you send. Parse it as a decimal. See [Money and quantities](/concepts/money-and-quantities/).
      - `calc_tax` (integer | string, required): The line's tax. Thousandths, an older encoding: `10800` is 10.80. A whole number of thousandths comes back as a JSON integer; an amount finer than a thousandth comes back as a decimal string, such as `"12656.3"`, whatever headers you send. Parse it as a decimal. See [Money and quantities](/concepts/money-and-quantities/).
      - `calc_total` (integer | string, required): `calc_subtotal` plus `calc_tax`. Thousandths, an older encoding: `10800` is 10.80. A whole number of thousandths comes back as a JSON integer; an amount finer than a thousandth comes back as a decimal string, such as `"12656.3"`, whatever headers you send. Parse it as a decimal. See [Money and quantities](/concepts/money-and-quantities/).
      - `id` (string, required): The line's id.
      - `non_inventory` (boolean, required): true for a line that doesn't track stock.
      - `price` (integer | string, required): The unit price. Thousandths, an older encoding: `10800` is 10.80. A whole number of thousandths comes back as a JSON integer; an amount finer than a thousandth comes back as a decimal string, such as `"12656.3"`, whatever headers you send. Parse it as a decimal. See [Money and quantities](/concepts/money-and-quantities/).
      - `product` (object, required): What the line buys, flat. `id` is a catalog item id such as `smr-10001` for `db_catalog`, or the hash id of your product or variant for `db_simple`, `user_simple` and `db_variant`; `name` is the line's own name for `custom`, `fee`, `fees` and `customer_provide_product`. Older lines can have `cart_item` or `order_item`, with that record's id. `size` and `color` appear only when the line has them.
        - `type` (string, required): The product kind, such as `db_catalog`, `user_simple`, `db_variant`, `custom` or `fee`.
        - `color` (string, optional)
        - `id` (string, optional)
        - `name` (string | null, optional)
        - `size` (string, optional)
      - `quantity` (integer | string, required): The quantity **still to receive** (ordered minus received), not the quantity ordered. A whole number is a JSON integer; a fraction is a decimal string, such as `"1.25"`.
      - `received_total` (integer | string, required): The quantity received so far. A whole number is a JSON integer; a fraction is a decimal string, such as `"1.25"`.
      - `tax_total` (integer | string, required): The line's own tax rate, used when `tax_mode` is `line_level`, as a percentage ×1000: `8000` is 8% (0 when the line has none). A rate that is not a whole number comes back as a decimal string.
      - `position` (integer | string | null, optional): The line's place on the purchase order, or null. A whole number is a JSON integer; a fraction is a decimal string, such as `"1.25"`.
  - `scoped_id` (string, required): The purchase order number people see, such as `PO0042`. Not an id.
  - `status_id` (string | null, required)
  - `trashed` (boolean, required)
  - `billing_address` (object | null, optional): The billing address.
    - `address1` (string | null, required)
    - `id` (string, required)
    - `is_current_source` (boolean, required): False for deleted, replaced, Shopify or redacted addresses. Only current addresses can be picked for new documents or edited.
    - `is_primary` (boolean, required)
    - `label` (string | null, required)
    - `revision_number` (integer, required): Goes up by one on every saved change. Send it back as `base_revision_number` when updating the address to protect against concurrent edits.
    - `role` (string, required): `billing` or `shipping` for customer addresses.
    - `trashed` (boolean, required)
    - `address2` (string | null, optional)
    - `address_family_id` (string | null, optional): Shared by an address and the copies made when it was edited after use.
    - `city` (string | null, optional)
    - `copied_from_address_id` (string | null, optional)
    - `country` (string | null, optional)
    - `country_code` (string | null, optional)
    - `customer_id` (string, optional): Customer addresses only: the customer's id.
    - `external_type` (string | null, optional): `shopify` for an address synced from Shopify.
    - `hold_stock` (boolean, optional): Company locations only; false for customer addresses.
    - `is_default_warehouse` (boolean, optional): Company locations only; false for customer addresses.
    - `is_external` (boolean, optional): True for an address synced from Shopify; such addresses are read-only.
    - `nexus` (boolean, optional): Company locations only; false for customer addresses.
    - `note` (string | null, optional)
    - `owner_id` (string | null, optional): The owner's id: for a customer address, the customer's id (the same as `customer_id`).
    - `owner_type` (string, optional): `customer` for customer addresses. One of: `"customer"`, `"team"`, `"unknown"`.
    - `postal_can_validate` (boolean, optional)
    - `postal_is_valid` (boolean, optional)
    - `postal_validated` (boolean, optional): True once YoPrint has checked the address with its address validator. `postal_can_validate` says whether it could be checked and `postal_is_valid` gives the result. Any edit resets it to false.
    - `postcode` (string | null, optional)
    - `source` (string, optional): `customer` for customer addresses. One of: `"customer"`, `"company_location"`, `"custom"`, `"unknown"`.
    - `state` (string | null, optional)
    - `state_code` (string | null, optional)
    - `superseded_at` (string (date-time) | null, optional)
    - `superseded_by_address_id` (string | null, optional): Set on an old copy: the address that replaced it.
    - `used_by_active_profile` (boolean, optional): True when a brand profile uses it as Ship From. Sent on lists, reads, updates and customer answers; not on create or set-primary.
  - `billing_address_id` (string | null, optional)
  - `created_at` (string (date-time) | null, optional)
  - `created_by` (string | null, optional): The team member who created it.
  - `creator` (object | null, optional): The team member who created it: `id`, `name`, `email`, `avatar_url`, `timezone` and a few profile flags.
    - `email` (string | null, optional)
    - `id` (string, optional)
    - `name` (string | null, optional)
  - `doc_viewer_url` (string, optional): A link that opens the purchase order in the document viewer, with the same kind of token.
  - `due_date` (string (date-time) | null, optional)
  - `internal_note` (string | null, optional)
  - `is_custom_warehouse` (boolean, optional): true when the delivery address is not one of the team's company locations.
  - `issue_date` (string (date-time) | null, optional)
  - `name` (string | null, optional)
  - `pdf` (array of objects, optional): PDF links: the purchase order detail, its 4" x 6" label, and the team's custom purchase order templates. Each `url` carries a token like `pdf_url`.
    - Each item:
      - `description` (string | null, optional)
      - `label` (string, optional)
      - `url` (string, optional)
  - `pdf_url` (string, optional): A link that downloads the purchase order PDF. Its token stops working 9,000 hours (about a year) after the answer was made, and anyone with the link can open the PDF: don't share it.
  - `quickbook_disable_sync` (boolean, optional): true when QuickBooks sync is off for this purchase order.
  - `quickbook_last_error_sync` (string (date-time) | null, optional)
  - `quickbook_last_success_sync` (string (date-time) | null, optional)
  - `status` (object | null, optional): The purchase order's status, as List purchase order statuses returns it: `id`, `role` (`purchase_order`), `type` (`start`, `pre_production`, `in_progress` or `end`), `label`, `customer_label`, `color`, `position` and `trashed`.
    - `id` (string, optional)
    - `label` (string, optional)
    - `role` (string, optional)
    - `type` (string, optional): One of: `"start"`, `"pre_production"`, `"in_progress"`, `"end"`.
  - `status_completion_date` (string (date-time) | null, optional): When it was moved to its final (`end`) status; null otherwise.
  - `status_last_change_date` (string (date-time) | null, optional)
  - `tags` (array of objects, optional)
    - Each item:
      - `color_hex` (string | null, required): Such as `#2563EB`. Null for a tag saved without a color.
      - `id` (string, required)
      - `name` (string, required)
  - `tax_mode` (string | null, optional): `order_level`: one `tax_rate` for the purchase order, and each line is taxable or not. `line_level`: each line has its own rate (`tax_total`).
  - `tax_rate` (string | null, optional): For `order_level`: the rate as a decimal string fraction with 6 decimals, `"0.080000"` is 8%. Null when not set.
  - `team` (object, optional): Your team, as the shared purchase order answer carries it: `id`, `name`, `slug`, contact details, the public ids of its enabled payment gateways, and `plan`. Nothing here is specific to the purchase order.
  - `updated_at` (string (date-time) | null, optional)
  - `vendor` (object | null, optional): The vendor with its contacts, as Get a vendor returns it.
    - `id` (string, required)
    - `name` (string | null, required)
    - `trashed` (boolean, required): True when the vendor is deleted.
    - `account_number` (string | null, optional)
    - `contacts` (array of objects, optional): Every contact, deleted ones included.
      - Each item:
        - `created_at` (string (date-time), required)
        - `first_name` (string | null, required)
        - `id` (string, required)
        - `is_primary` (boolean, required)
        - `trashed` (boolean, required): True when the contact is deleted.
        - `updated_at` (string (date-time), required)
        - `customer_id` (string, optional): Customer contacts only.
        - `default_receive_sms` (boolean, optional)
        - `email` (string | null, optional)
        - `external_type` (string | null, optional)
        - `fax` (string | null, optional)
        - `fax_country` (string | null, optional)
        - `is_external` (boolean, optional): True for a contact synced from Shopify; such contacts are read-only.
        - `last_name` (string | null, optional)
        - `phone` (string | null, optional)
        - `phone_country` (string | null, optional)
        - `send_invoice` (boolean, optional): Customer contacts only.
        - `vendor_id` (string, optional): Vendor contacts only.
    - `default_vendor_note` (string | null, optional)
    - `internal_notes` (string | null, optional)
    - `website` (string | null, optional)
  - `vendor_id` (string | null, optional)
  - `vendor_note` (string | null, optional): The note printed for the vendor.
  - `version` (integer, optional): 2 for purchase orders made in the current editor; 1 for older ones.
  - `warehouse` (object | null, optional): The delivery address, usually one of the team's company locations. Same fields as `data.billing_address` above.
  - `warehouse_can_hold_stock` (boolean, optional): true when the delivery address is set and holds stock, so receiving adds stock there.
  - `warehouse_id` (string | null, optional): Where the goods are delivered.
- `status` (integer, required)
- `success` (boolean, required)

Example response:

```json
{
  "data": {
    "billing_address": {
      "address1": "100 Example Street",
      "address2": null,
      "address_family_id": null,
      "city": "Springfield",
      "copied_from_address_id": null,
      "country": "United States",
      "country_code": "US",
      "external_type": null,
      "hold_stock": true,
      "id": "GXCU6IMtXEV9Qn",
      "is_current_source": true,
      "is_default_warehouse": true,
      "is_external": false,
      "is_primary": true,
      "label": "Main warehouse",
      "nexus": true,
      "note": null,
      "owner_id": "g9NXwcqzTRIABH",
      "owner_type": "team",
      "postal_can_validate": false,
      "postal_is_valid": false,
      "postal_validated": false,
      "postcode": "62701",
      "revision_number": 1,
      "role": "store_location",
      "source": "company_location",
      "state": "Illinois",
      "state_code": "IL",
      "superseded_at": null,
      "superseded_by_address_id": null,
      "trashed": false
    },
    "billing_address_id": "GXCU6IMtXEV9Qn",
    "calc_subtotal": 120000,
    "calc_tax": 9600,
    "calc_total": 129600,
    "created_at": "2026-11-02T09:00:00+00:00",
    "created_by": "lOURnovd3hkIQI",
    "creator": {
      "avatar_url": null,
      "email": "alex@example.com",
      "id": "lOURnovd3hkIQI",
      "name": "Alex Rivera",
      "timezone": "America/Chicago"
    },
    "doc_viewer_url": "https://example.com/documents/purchase-order",
    "due_date": "2026-11-09T00:00:00+00:00",
    "id": "12vR7n9WUYn1CJ",
    "internal_note": null,
    "is_custom_warehouse": false,
    "issue_date": "2026-11-02T00:00:00+00:00",
    "items": [
      {
        "calc_subtotal": 108000,
        "calc_tax": 8640,
        "calc_total": 116640,
        "id": "szhbjt6FGUoOcW",
        "non_inventory": false,
        "position": 1,
        "price": 4500,
        "product": {
          "color": "Black",
          "id": "smr-10001",
          "size": "L",
          "type": "db_catalog"
        },
        "quantity": 0,
        "received_total": 24,
        "tax_total": 0
      },
      {
        "calc_subtotal": 12000,
        "calc_tax": 960,
        "calc_total": 12960,
        "id": "gJn3IswvuEsoUd",
        "non_inventory": false,
        "position": 2,
        "price": 12000,
        "product": {
          "name": "Freight",
          "type": "fee"
        },
        "quantity": 1,
        "received_total": 0,
        "tax_total": 0
      }
    ],
    "name": "Fall restock",
    "pdf": [
      {
        "description": "Purchase Order Detail",
        "label": "Purchase Order Detail",
        "url": "https://example.com/pdf/purchase-order.pdf"
      },
      {
        "description": "Purchase Order Label",
        "label": "Purchase Order Label (4\" x 6\")",
        "url": "https://example.com/pdf/purchase-order-label.pdf"
      }
    ],
    "pdf_url": "https://example.com/pdf/purchase-order.pdf",
    "quickbook_disable_sync": false,
    "quickbook_last_error_sync": null,
    "quickbook_last_success_sync": null,
    "scoped_id": "PO0042",
    "status": {
      "color": "#4caf50",
      "customer_label": "Completed",
      "id": "mOR2M727uLiHaX",
      "label": "Received",
      "position": 3,
      "role": "purchase_order",
      "trashed": false,
      "type": "end"
    },
    "status_completion_date": "2026-11-06T09:00:00+00:00",
    "status_id": "mOR2M727uLiHaX",
    "status_last_change_date": "2026-11-06T09:00:00+00:00",
    "tags": [],
    "tax_mode": "order_level",
    "tax_rate": "0.080000",
    "team": {
      "id": "g9NXwcqzTRIABH",
      "name": "Example Print Shop",
      "payment_gateway": [],
      "slug": "your-team",
      "timezone": "America/Chicago"
    },
    "trashed": false,
    "updated_at": "2026-11-06T09:00:00+00:00",
    "vendor": {
      "account_number": "ACCT-1001",
      "contacts": [
        {
          "created_at": "2026-11-01T09:00:00+00:00",
          "default_receive_sms": false,
          "email": "sam@example.com",
          "external_type": null,
          "fax": null,
          "fax_country": null,
          "first_name": "Sam",
          "id": "fopJXRDkZ8Qk7S",
          "is_external": false,
          "is_primary": true,
          "last_name": "Lee",
          "phone": "+1 201 555 0123",
          "phone_country": "US",
          "trashed": false,
          "updated_at": "2026-11-01T09:00:00+00:00",
          "vendor_id": "MFwvMEIK7ud89l"
        }
      ],
      "default_vendor_note": null,
      "id": "MFwvMEIK7ud89l",
      "internal_notes": null,
      "name": "Example Blanks Co",
      "trashed": false,
      "website": "https://example.com"
    },
    "vendor_id": "MFwvMEIK7ud89l",
    "vendor_note": "Deliver to the back dock.",
    "version": 2,
    "warehouse": {
      "address1": "100 Example Street",
      "address2": null,
      "address_family_id": null,
      "city": "Springfield",
      "copied_from_address_id": null,
      "country": "United States",
      "country_code": "US",
      "external_type": null,
      "hold_stock": true,
      "id": "GXCU6IMtXEV9Qn",
      "is_current_source": true,
      "is_default_warehouse": true,
      "is_external": false,
      "is_primary": true,
      "label": "Main warehouse",
      "nexus": true,
      "note": null,
      "owner_id": "g9NXwcqzTRIABH",
      "owner_type": "team",
      "postal_can_validate": false,
      "postal_is_valid": false,
      "postal_validated": false,
      "postcode": "62701",
      "revision_number": 1,
      "role": "store_location",
      "source": "company_location",
      "state": "Illinois",
      "state_code": "IL",
      "superseded_at": null,
      "superseded_by_address_id": null,
      "trashed": false
    },
    "warehouse_can_hold_stock": true,
    "warehouse_id": "GXCU6IMtXEV9Qn"
  },
  "status": 200,
  "success": true
}
```

## Errors

- `401` The API key is missing or wrong (`unauthenticated`).
- `403` You are not allowed to do this. `api_access_not_in_plan`: your team's plan doesn't include API access (Pro and above, with an active subscription or trial), so every request answers this until the team upgrades. When your role lacks the permission the operation needs, the body is only `{"message": "Insufficient Permission"}`. Other refusals use the error envelope, with `unauthorized` or `user_is_not_authorized`.
  - When: Your role has no Manage Purchase Order permission to update. This 403 has a short body, `{"message": "Insufficient Permission"}`, with no `success` or `error`. Fix: Ask a team owner for a Manage Purchase Order level that can update.
- `404` Not found. `page_not_found`: the team slug is unknown, you are not a member of the team, or an id in the path is not one of this team's. `code: null`, with a message that starts "No query results for": there is no record with that id.
  - When: No live purchase order with this id in the team, or your role updates only purchase orders you created or are assigned to and this is not one of them. Nothing changes. `error.code` is null and `error.message` names the record type. Fix: Check the id, or ask to be assigned to the purchase order.
- `422` The request was refused and changed nothing. `validation_failed` lists what to fix in `error.fields`; other codes name a business rule, explained in `error.message`. A few checks answer with only `{"message": "..."}`. An operation that calls another service (a payment provider, a carrier, an accounting app) can answer `connect_timeout` ("Please retry again") when that service does not answer in time: the change may or may not have happened there, so read the record back before you retry.
  - When: `status_id` is missing or is not an id. `error.code` is `validation_failed`. Fix: Send the `id` of a purchase order status.
  - When: `status_id` is not one of this team's purchase order statuses (`error.code` `validation_error`, "Invalid status_id for purchase order"). Fix: Pick a status from List purchase order statuses.
  - When: Moving to the `end` status while lines are still to be received ("Purchase Order cannot be completed without receiving all the items"), or while the delivery location is no longer current ("Purchase Order cannot be completed with an inactive warehouse location"). `error.code` is `validation_error`; nothing changes. Fix: Receive the remaining items in YoPrint first, or pick a status that is not `end`.

## Related

- [Get a purchase order](https://developer.yoprint.com/reference/purchase-orders/v1-purchase-order-show.md): `GET /v1/api/store/{team_slug}/purchase_order/{hash_id}`
- [List purchase order statuses](https://developer.yoprint.com/reference/statuses/v1-status-list-purchase-order.md): `GET /v1/api/store/{team_slug}/setting/status/purchase_order`
- [List a purchase order's history](https://developer.yoprint.com/reference/purchase-orders/v1-purchase-order-history.md): `GET /v1/api/store/{team_slug}/purchase_order/{hash_id}/history`
