> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.getply.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.getply.com/_mcp/server.

# Create Purchase Order

POST https://api.getply.com/api/v1/purchase-orders
Content-Type: application/json

Creates a new purchase order. Items can be included in the request body
or added later via the items endpoints.

Requires scope: `purchase_orders:write`

Reference: https://docs.getply.com/api-reference/api-reference/purchase-orders/create

## Authentication

- `API_KEY` header (required) — API Key authentication via header

## Servers

- `https://api.getply.com` (production, default)
- `https://sandbox-api.getply.com` (sandbox)

## Request

### Body (application/json)

This endpoint expects a CreatePurchaseOrderRequest.

- `orderNumber` (string, required) — Order reference number. Required. Maximum 255 characters.
- `supplierId` (UUID, optional) — ID of the supplier for this order.
- `locationId` (UUID, optional) — ID of the destination location.
- `note` (string, optional) — Optional free-text note for the supplier.
- `dueDate` (date, optional) — Expected delivery date (ISO 8601 date, e.g. `2026-04-15`).
- `items` (list of CreatePurchaseOrderItemRequest, optional) — Initial line items. Items can also be added after creation.

## Response

### 201

- `id` (UUID, required) — Unique identifier for the purchase order.
- `itemCount` (integer, required) — Number of line items in this purchase order.
- `orderNumber` (string, optional) — Human-readable order reference number (e.g. `PO-1042`).
- `status` (enum, optional) — Current lifecycle status of the purchase order.
  - Allowed values: `DRAFT`, `WAITING_FOR_APPROVAL`, `SENT`, `CONFIRMED`, `PAID`, `PARTIALLY_RECEIVED`, `RECEIVED`, `ARCHIVED`, `CANCELED`, `EXPIRED`, `DECLINED`
- `type` (string, optional) — Classification of the order: `REPLENISHMENT`, `MATERIALS_ORDER` or `OTHER`. Not settable through this API, so orders created here always report `null`.
- `totalCost` (double, optional) — Grand total cost of all items in USD.
- `subtotal` (double, optional) — Sum of the order's line items: `quantity * price` for every item still on the order, with removed items excluded. Always computed from the items when the order is read, so it is correct no matter how the order was created. Excludes tax, shipping and discounts, which this API does not expose. It is `0` only on an order that has no items.
- `note` (string, optional) — Free-text note for the supplier.
- `supplier` (ResourceRef, optional) — Reference to the supplier for this order.
- `location` (ResourceRef, optional) — Reference to the destination location.
- `dueDate` (date, optional) — ISO 8601 date when the order is expected (e.g. `2026-04-15`).
- `receivedAt` (datetime, optional) — ISO 8601 timestamp when the order was marked received. `null` if not yet received.
- `assignedUserIds` (list of UUID, optional) — IDs of users assigned to this purchase order. Returned by get-by-id, create and update; the list endpoint omits it to keep the payload light, so it is `null` on every row there and only there.
- `createdAt` (datetime, optional) — ISO 8601 timestamp when the record was created.
- `updatedAt` (datetime, optional) — ISO 8601 timestamp when the record was last updated.

## Errors

### 400 Bad Request Error

The request was malformed or failed validation. Check the `message` field for details on which fields are invalid.

- `code` (string, required) — Machine-readable error code (e.g. `NOT_FOUND`, `VALIDATION_ERROR`).
- `message` (string, required) — Human-readable description of what went wrong.
- `status` (integer, required) — HTTP status code.
- `timestamp` (string, required) — ISO 8601 timestamp when the error occurred (e.g. `2026-03-20T14:30:00`).
- `path` (string, required) — The request path that triggered the error.

### 401 Unauthorized Error

The `API_KEY` header is missing or the key is invalid. Verify your API key in the Ply dashboard under Settings > Workspace > API Keys.

- `code` (string, required) — Machine-readable error code (e.g. `NOT_FOUND`, `VALIDATION_ERROR`).
- `message` (string, required) — Human-readable description of what went wrong.
- `status` (integer, required) — HTTP status code.
- `timestamp` (string, required) — ISO 8601 timestamp when the error occurred (e.g. `2026-03-20T14:30:00`).
- `path` (string, required) — The request path that triggered the error.

### 403 Forbidden Error

The authenticated API key does not have permission to perform this action.

- `code` (string, required) — Machine-readable error code (e.g. `NOT_FOUND`, `VALIDATION_ERROR`).
- `message` (string, required) — Human-readable description of what went wrong.
- `status` (integer, required) — HTTP status code.
- `timestamp` (string, required) — ISO 8601 timestamp when the error occurred (e.g. `2026-03-20T14:30:00`).
- `path` (string, required) — The request path that triggered the error.

### 404 Not Found Error

The requested resource does not exist or has been deleted.

- `code` (string, required) — Machine-readable error code (e.g. `NOT_FOUND`, `VALIDATION_ERROR`).
- `message` (string, required) — Human-readable description of what went wrong.
- `status` (integer, required) — HTTP status code.
- `timestamp` (string, required) — ISO 8601 timestamp when the error occurred (e.g. `2026-03-20T14:30:00`).
- `path` (string, required) — The request path that triggered the error.

### 429 Rate Limit Error

You have exceeded the rate limit. Two limits apply, both pooled per business — creating additional API keys does not raise either: - 60 requests per minute - 10,000 requests per day Any response that passes authentication carries `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`; requests rejected earlier, such as a missing key or a missing scope, carry none of them. On a `429`, wait the number of seconds given in `Retry-After` before retrying, and back off further if you get a second one.

- `code` (string, required) — Machine-readable error code (e.g. `NOT_FOUND`, `VALIDATION_ERROR`).
- `message` (string, required) — Human-readable description of what went wrong.
- `status` (integer, required) — HTTP status code.
- `timestamp` (string, required) — ISO 8601 timestamp when the error occurred (e.g. `2026-03-20T14:30:00`).
- `path` (string, required) — The request path that triggered the error.

## Types

### CreatePurchaseOrderItemRequest

- `materialId` (UUID, required) — ID of the material to add. Required.
- `quantity` (double, required) — Quantity to order. Required. Must be greater than zero.
- `price` (double, required) — Cost per unit in USD. Required. Must be greater than or equal to zero.
- `materialVariationId` (UUID, optional) — ID of the material variation, if applicable.
- `note` (string, optional) — Optional note for this line item.

### ResourceRef

A reference to a related resource, containing its ID and display name.

- `id` (UUID, required) — Unique identifier of the referenced resource.
- `name` (string, optional) — Human-readable display name.

## Examples

**Request**

```json
{
  "orderNumber": "PO-1042",
  "supplierId": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
  "locationId": "c3d4e5f6-a7b8-9012-cdef-123456789012",
  "note": "Please ship to back warehouse entrance.",
  "dueDate": "2026-04-15",
  "items": [
    {
      "materialId": "e5f6a7b8-c9d0-1234-efab-345678901234",
      "quantity": 50,
      "price": 25
    }
  ]
}
```

**Response**

```json
{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "itemCount": 1,
  "orderNumber": "PO-1042",
  "status": "DRAFT",
  "type": null,
  "totalCost": 1250,
  "subtotal": 1250,
  "note": "Please ship to back warehouse entrance.",
  "supplier": {
    "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
    "name": "Acme Supply Co."
  },
  "location": {
    "id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
    "name": "Main Warehouse"
  },
  "dueDate": "2026-04-15",
  "receivedAt": null,
  "assignedUserIds": [],
  "createdAt": "2026-03-20T14:30:00",
  "updatedAt": "2026-03-20T14:30:00"
}
```

**SDK Code**

```python
import requests

url = "https://api.getply.com/api/v1/purchase-orders"

payload = {
    "orderNumber": "PO-1042",
    "supplierId": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
    "locationId": "c3d4e5f6-a7b8-9012-cdef-123456789012",
    "note": "Please ship to back warehouse entrance.",
    "dueDate": "2026-04-15",
    "items": [
        {
            "materialId": "e5f6a7b8-c9d0-1234-efab-345678901234",
            "quantity": 50,
            "price": 25
        }
    ]
}
headers = {
    "API_KEY": "<apiKey>",
    "Content-Type": "application/json"
}

response = requests.post(url, json=payload, headers=headers)

print(response.json())
```

```javascript
const url = 'https://api.getply.com/api/v1/purchase-orders';
const options = {
  method: 'POST',
  headers: {API_KEY: '<apiKey>', 'Content-Type': 'application/json'},
  body: '{"orderNumber":"PO-1042","supplierId":"b2c3d4e5-f6a7-8901-bcde-f12345678901","locationId":"c3d4e5f6-a7b8-9012-cdef-123456789012","note":"Please ship to back warehouse entrance.","dueDate":"2026-04-15","items":[{"materialId":"e5f6a7b8-c9d0-1234-efab-345678901234","quantity":50,"price":25}]}'
};

try {
  const response = await fetch(url, options);
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error(error);
}
```

```go
package main

import (
	"fmt"
	"strings"
	"net/http"
	"io"
)

func main() {

	url := "https://api.getply.com/api/v1/purchase-orders"

	payload := strings.NewReader("{\n  \"orderNumber\": \"PO-1042\",\n  \"supplierId\": \"b2c3d4e5-f6a7-8901-bcde-f12345678901\",\n  \"locationId\": \"c3d4e5f6-a7b8-9012-cdef-123456789012\",\n  \"note\": \"Please ship to back warehouse entrance.\",\n  \"dueDate\": \"2026-04-15\",\n  \"items\": [\n    {\n      \"materialId\": \"e5f6a7b8-c9d0-1234-efab-345678901234\",\n      \"quantity\": 50,\n      \"price\": 25\n    }\n  ]\n}")

	req, _ := http.NewRequest("POST", url, payload)

	req.Header.Add("API_KEY", "<apiKey>")
	req.Header.Add("Content-Type", "application/json")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
```

```ruby
require 'uri'
require 'net/http'

url = URI("https://api.getply.com/api/v1/purchase-orders")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Post.new(url)
request["API_KEY"] = '<apiKey>'
request["Content-Type"] = 'application/json'
request.body = "{\n  \"orderNumber\": \"PO-1042\",\n  \"supplierId\": \"b2c3d4e5-f6a7-8901-bcde-f12345678901\",\n  \"locationId\": \"c3d4e5f6-a7b8-9012-cdef-123456789012\",\n  \"note\": \"Please ship to back warehouse entrance.\",\n  \"dueDate\": \"2026-04-15\",\n  \"items\": [\n    {\n      \"materialId\": \"e5f6a7b8-c9d0-1234-efab-345678901234\",\n      \"quantity\": 50,\n      \"price\": 25\n    }\n  ]\n}"

response = http.request(request)
puts response.read_body
```

```java
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.post("https://api.getply.com/api/v1/purchase-orders")
  .header("API_KEY", "<apiKey>")
  .header("Content-Type", "application/json")
  .body("{\n  \"orderNumber\": \"PO-1042\",\n  \"supplierId\": \"b2c3d4e5-f6a7-8901-bcde-f12345678901\",\n  \"locationId\": \"c3d4e5f6-a7b8-9012-cdef-123456789012\",\n  \"note\": \"Please ship to back warehouse entrance.\",\n  \"dueDate\": \"2026-04-15\",\n  \"items\": [\n    {\n      \"materialId\": \"e5f6a7b8-c9d0-1234-efab-345678901234\",\n      \"quantity\": 50,\n      \"price\": 25\n    }\n  ]\n}")
  .asString();
```

```php
<?php
require_once('vendor/autoload.php');

$client = new \GuzzleHttp\Client();

$response = $client->request('POST', 'https://api.getply.com/api/v1/purchase-orders', [
  'body' => '{
  "orderNumber": "PO-1042",
  "supplierId": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
  "locationId": "c3d4e5f6-a7b8-9012-cdef-123456789012",
  "note": "Please ship to back warehouse entrance.",
  "dueDate": "2026-04-15",
  "items": [
    {
      "materialId": "e5f6a7b8-c9d0-1234-efab-345678901234",
      "quantity": 50,
      "price": 25
    }
  ]
}',
  'headers' => [
    'API_KEY' => '<apiKey>',
    'Content-Type' => 'application/json',
  ],
]);

echo $response->getBody();
```

```csharp
using RestSharp;

var client = new RestClient("https://api.getply.com/api/v1/purchase-orders");
var request = new RestRequest(Method.POST);
request.AddHeader("API_KEY", "<apiKey>");
request.AddHeader("Content-Type", "application/json");
request.AddParameter("application/json", "{\n  \"orderNumber\": \"PO-1042\",\n  \"supplierId\": \"b2c3d4e5-f6a7-8901-bcde-f12345678901\",\n  \"locationId\": \"c3d4e5f6-a7b8-9012-cdef-123456789012\",\n  \"note\": \"Please ship to back warehouse entrance.\",\n  \"dueDate\": \"2026-04-15\",\n  \"items\": [\n    {\n      \"materialId\": \"e5f6a7b8-c9d0-1234-efab-345678901234\",\n      \"quantity\": 50,\n      \"price\": 25\n    }\n  ]\n}", ParameterType.RequestBody);
IRestResponse response = client.Execute(request);
```

```swift
import Foundation

let headers = [
  "API_KEY": "<apiKey>",
  "Content-Type": "application/json"
]
let parameters = [
  "orderNumber": "PO-1042",
  "supplierId": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
  "locationId": "c3d4e5f6-a7b8-9012-cdef-123456789012",
  "note": "Please ship to back warehouse entrance.",
  "dueDate": "2026-04-15",
  "items": [
    [
      "materialId": "e5f6a7b8-c9d0-1234-efab-345678901234",
      "quantity": 50,
      "price": 25
    ]
  ]
] as [String : Any]

let postData = JSONSerialization.data(withJSONObject: parameters, options: [])

let request = NSMutableURLRequest(url: NSURL(string: "https://api.getply.com/api/v1/purchase-orders")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "POST"
request.allHTTPHeaderFields = headers
request.httpBody = postData as Data

let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    print(error as Any)
  } else {
    let httpResponse = response as? HTTPURLResponse
    print(httpResponse)
  }
})

dataTask.resume()
```