Skip to content

Take an order from quote to payment

Open .md

This guide takes one order from a customer’s request to a paid sales order, one operation per step. Every step shows the request and a shortened answer, and says which value to keep for the next step. Each step links to the guide with the full details: Create and change orders, Add line items, Invoice an order and Take payments and refunds. It uses the words from Quotes and sales orders and Invoices and payments. Read those first if draft, publish or convert are new.

The order is one record the whole way. Each step changes what it is, never its id:

  1. Draft quoteSteps 1 and 2: create it, then add a line
  2. QuoteStep 4: send it to the customer
  3. Sales orderSteps 6 to 8: invoice it, record a payment, read the balance
Keep the id from step 1: every later step uses it.

You need your API key, your Team ID and a customer’s id. The Quickstart gets the first two and creates a test customer. Put all three in your shell, with your own values in place of these:

Terminal window
export YOPRINT_API_KEY="YOUR_API_KEY"
export YOPRINT_TEAM="your-team"
export CUSTOMER_ID="VwYKuTP9vd8Dfo"

Each request below uses these variables. Requests with a body use -d @- <<EOF ... EOF, so the shell fills the variables in the body too. In sales_order/… paths, {hash_id}, {order_unique_key} and {unique_key} are all the order’s id. In invoice/… paths, they’re the invoice’s id from step 6. See IDs.

POST Create a sales order or quote /v2/api/store/{team_slug}/v2/sales_order/new
Terminal window
curl "https://secure.yoprint.com/v2/api/store/$YOPRINT_TEAM/v2/sales_order/new" \
-H "Authorization: Bearer $YOPRINT_API_KEY" \
-H "Accept: application/json" \
-H "X-Yoprint-Decimal-Format: exact-v1" \
-H "Content-Type: application/json" \
-d @- <<EOF
{
"object_type": "quote",
"customer_id": "$CUSTOMER_ID",
"customer_po_number": "PO-1001"
}
EOF

The answer is the new draft. Shortened:

{
"status": 200,
"success": true,
"data": {
"id": "V4keXwwxMQyXw7",
"object_type": "quote",
"internal_status": "draft",
"customer_id": "VwYKuTP9vd8Dfo",
"customer": { "id": "VwYKuTP9vd8Dfo", "name": "TEST Northwind Apparel" },
"customer_po_number": "PO-1001",
"order_item_cart_total": { "value": 0, "dp": 0 }
}
}

Keep data.id. Every later step uses it:

Terminal window
export ORDER_ID="V4keXwwxMQyXw7"
  • object_type is quote or sales_order. Without it, YoPrint creates a sales order. To skip the quote, send sales_order, then skip steps 4 and 5.
  • The new record is a draft. Its dates come from the team’s settings, its pricing group and payment terms from the customer’s defaults or the team’s, and its currency is the team’s.

Add one line, here a custom line item with a name and a price:

POST Add a line item to a sales order or quote /v2/api/store/{team_slug}/v2/sales_order/{order_unique_key}/order_item
Terminal window
curl "https://secure.yoprint.com/v2/api/store/$YOPRINT_TEAM/v2/sales_order/$ORDER_ID/order_item" \
-H "Authorization: Bearer $YOPRINT_API_KEY" \
-H "Accept: application/json" \
-H "X-Yoprint-Decimal-Format: exact-v1" \
-H "Content-Type: application/json" \
-d '{
"product": {
"type": "custom",
"properties": { "name": "Screen printed tee", "customer_supplied": false }
},
"pricing": {
"type": "custom_product",
"product": { "override": true, "total": { "value": 1250, "dp": 2 } }
},
"quantity": { "type": "unit", "total": { "value": 24, "dp": 0 } },
"job": { "type": "standalone" },
"position": 100000
}'

That’s 24 pieces at 12.50 each. Amounts and quantities are decimal pairs (value ÷ 10^dp: 1250 with dp 2 is 12.50, and 24 with dp 0 is 24). See Money and quantities.

The answer wraps the new line in item. Shortened:

{
"status": 200,
"success": true,
"data": {
"action_performed": "CREATED",
"has_changes": true,
"object_type": "order_item",
"item": {
"id": "uEm77FEbwZEMvu",
"order_id": "V4keXwwxMQyXw7",
"type": "custom",
"position": 100000,
"quantity": { "type": "unit", "total": { "value": 24, "dp": 0 } },
"unit_price": { "value": 12500000, "dp": 6 },
"subtotal": { "value": 3000000, "dp": 4 }
}
}
}

Keep data.item.id, the line’s id. Step 6 bills this line by it:

Terminal window
export LINE_ID="uEm77FEbwZEMvu"

The answer keeps prices at 6 decimal places and the subtotal at 4, so unit_price is 12.500000 and subtotal is 300.0000. Always read dp.

"override": true charges exactly the price you send. Without it, YoPrint works the price out from the cost, with the order’s pricing group markup or a matching pricing rule, so also send the cost (pricing.product_cost) to keep profit and margin right. See How a line’s price is worked out, and Add line items for the other kinds of line.

Publishing makes the draft a real quote. From now on its changes are recorded in its history, and it can be sent and approved. It fires quote.created.

PATCH Publish a draft sales order or quote /v2/api/store/{team_slug}/v2/sales_order/{order_unique_key}/publish
Terminal window
curl -X PATCH "https://secure.yoprint.com/v2/api/store/$YOPRINT_TEAM/v2/sales_order/$ORDER_ID/publish" \
-H "Authorization: Bearer $YOPRINT_API_KEY" \
-H "Accept: application/json" \
-H "X-Yoprint-Decimal-Format: exact-v1"

When it worked, data.valid is true:

{
"status": 200,
"success": true,
"data": {
"id": "V4keXwwxMQyXw7",
"valid": true,
"validation": {
"has_customer": true,
"has_order_items": true,
"has_sellable_items": true,
"unsellable_items": []
}
}
}

Publishing answers 200 even when it can’t publish. Check data.valid. When a check fails, it’s false, the draft stays a draft, and validation says which check failed:

  • has_customer: false if the draft has no customer (for example, if created with an unknown customer_id). Use the change_customer action to set one.
  • has_order_items: false if no lines have been added.
  • has_sellable_items: false if a line uses a phased-out or deleted product, variant or color. unsellable_items lists each affected line with its reason (product_phased_out, product_deleted, variant_phased_out or variant_deleted).
  • removed_pricing_group: names the draft’s pricing group when it was removed or disabled. Choose another with the change_pricing_group action.

Publishing a quote that’s already published answers 200 with "success": false and error.code action_not_allowed, and changes nothing.

To email the customer the quote, use the send_order_detail action. To ask for their approval, create an approval:

POST Ask the customer to approve an order /v2/api/store/{team_slug}/v2/sales_order/{hash_id}/approval

Both email the customer at once, and count toward your team’s email limits. Sending more than 60 customer emails in a minute blocks your whole team’s email until YoPrint support lifts it. See Limits.

When the customer accepts, convert it. The record keeps its id and becomes a sales order:

PUT Run an action on a sales order or quote /v2/api/store/{team_slug}/v2/sales_order/{order_unique_key}/action/{action_name}
Terminal window
curl -X PUT "https://secure.yoprint.com/v2/api/store/$YOPRINT_TEAM/v2/sales_order/$ORDER_ID/action/convert_to_sales_order" \
-H "Authorization: Bearer $YOPRINT_API_KEY" \
-H "Accept: application/json" \
-H "X-Yoprint-Decimal-Format: exact-v1" \
-H "Content-Type: application/json" \
-d '{}'

The answer is the order, now a sales order with the same id. Shortened:

{
"status": 200,
"success": true,
"data": {
"id": "V4keXwwxMQyXw7",
"object_type": "sales_order",
"internal_status": "published",
"order_item_cart_total": { "value": 3000000, "dp": 4 }
}
}
  • It starts in the team’s first sales order status. To choose another, send "status_id" with a sales order status id.
  • It fires quote.converted_to_sales_order.
  • Quotes synced from Shopify, or made for a customer synced from Shopify, can’t be converted.
POST Create an invoice for a sales order /v2/api/store/{team_slug}/v2/sales_order/{unique_key}/invoice
Terminal window
curl "https://secure.yoprint.com/v2/api/store/$YOPRINT_TEAM/v2/sales_order/$ORDER_ID/invoice" \
-H "Authorization: Bearer $YOPRINT_API_KEY" \
-H "Accept: application/json" \
-H "X-Yoprint-Decimal-Format: exact-v1" \
-H "Content-Type: application/json" \
-d '{ "issue_date": "2026-11-03T00:00:00Z" }'

The answer is the new, empty invoice. Its addresses come from the order. Shortened:

{
"status": 200,
"success": true,
"data": {
"id": "QRbvVd2ZKcvSTB",
"order_id": "V4keXwwxMQyXw7",
"issue_date": "2026-11-03T00:00:00+00:00",
"is_paid": false
}
}

Keep data.id, the invoice’s id:

Terminal window
export INVOICE_ID="QRbvVd2ZKcvSTB"

Then add the order’s line to it. An invoice line points at an order line by that line’s id: the data.item.id you kept in step 2.

POST Add a line to an invoice /v2/api/store/{team_slug}/v2/invoice/{unique_key}/item
Terminal window
curl "https://secure.yoprint.com/v2/api/store/$YOPRINT_TEAM/v2/invoice/$INVOICE_ID/item" \
-H "Authorization: Bearer $YOPRINT_API_KEY" \
-H "Accept: application/json" \
-H "X-Yoprint-Decimal-Format: exact-v1" \
-H "Content-Type: application/json" \
-d @- <<EOF
{
"product": { "type": "order_item", "properties": { "id": "$LINE_ID" } },
"quantity": { "type": "unit", "total": { "value": 24, "dp": 0 } },
"position": 100000
}
EOF

The answer wraps the invoice line in item, like step 2. Shortened:

{
"status": 200,
"success": true,
"data": {
"action_performed": "CREATED",
"has_changes": true,
"object_type": "invoice_item",
"item": {
"id": "nhsGsn84PyJBvK",
"invoice_id": "QRbvVd2ZKcvSTB",
"type": "order_item",
"product": { "type": "order_item", "properties": { "id": "uEm77FEbwZEMvu" } },
"quantity": { "type": "unit", "total": { "value": 24, "dp": 0 } }
}
}
}
  • One invoice can’t bill the same order line twice: a second time answers 422 “Invoice already has the same Order Item”.

  • An order can have several invoices, for example a deposit invoice and a final one.

  • To add many lines at once, use the bulk operation, or send them in item_actions when you create the invoice:

    POST Add, change and remove invoice lines at once /v2/api/store/{team_slug}/v2/invoice/{unique_key}/item/bulk
  • On an order with Auto Invoice, YoPrint keeps the invoice for you and refuses invoices you create yourself.

Get the id of the payment method you were paid with, such as cash or bank transfer. payment_method_id is optional, but without it the payment has no method:

GET List payment methods /v1/api/store/{team_slug}/setting/payment_method
Terminal window
curl "https://secure.yoprint.com/v1/api/store/$YOPRINT_TEAM/setting/payment_method" \
-H "Authorization: Bearer $YOPRINT_API_KEY" \
-H "Accept: application/json" \
-H "X-Yoprint-Decimal-Format: exact-v1"
{
"status": 200,
"success": true,
"data": [
{ "id": "StmZGqEqgq5tt6", "name": "Bank Transfer", "default": false, "trashed": false }
]
}

Pick one that isn’t trashed, and keep its id:

Terminal window
export PAYMENT_METHOD_ID="StmZGqEqgq5tt6"

Then record the payment on the sales order.

POST Record a payment on a sales order /v2/api/store/{team_slug}/v2/sales_order/{hash_id}/payment
Terminal window
curl "https://secure.yoprint.com/v2/api/store/$YOPRINT_TEAM/v2/sales_order/$ORDER_ID/payment" \
-H "Authorization: Bearer $YOPRINT_API_KEY" \
-H "Accept: application/json" \
-H "X-Yoprint-Decimal-Format: exact-v1" \
-H "Content-Type: application/json" \
-d @- <<EOF
{
"payment_method_id": "$PAYMENT_METHOD_ID",
"amount": 300000,
"date": "2026-11-03T15:00:00Z",
"reference_id": "Bank ref 1001"
}
EOF

The answer is the payment. Shortened:

{
"status": 200,
"success": true,
"data": {
"id": "hAwWd7vPrqjuHD",
"order_id": "V4keXwwxMQyXw7",
"type": "MANUAL",
"subtype": "PAYMENT",
"amount": 300000,
"payment_method_id": "StmZGqEqgq5tt6",
"reference_id": "Bank ref 1001",
"date": "2026-11-03T15:00:00+00:00"
}
}

Keep data.id if you’ll clean up afterwards:

Terminal window
export PAYMENT_ID="hAwWd7vPrqjuHD"

Recording a payment doesn’t charge anyone. It records money you already received, and the order must be published. It updates the order’s balance and invoices, and YoPrint doesn’t email the customer a receipt for it.

It fires these webhooks:

  • payment_received.any_amount, for every payment.
  • payment_received.full_amount, when nothing is left to pay.
  • payment_received.deposit_amount, only when the total paid is exactly the deposit. A payment that goes past the deposit doesn’t send it.

To act once the deposit is paid, listen to payment_received.any_amount, then read the order and check that deposit_amount_due is 0 or less.

To split a payment across the order’s invoices, create a payment link. Despite the name, it isn’t a page your customer opens, and it moves no money:

POST Create a payment link for a sales order /v2/api/store/{team_slug}/v2/sales_order/{hash_id}/payment_link GET Get a sales order or quote /v2/api/store/{team_slug}/v2/sales_order/{order_unique_key}
Terminal window
curl "https://secure.yoprint.com/v2/api/store/$YOPRINT_TEAM/v2/sales_order/$ORDER_ID" \
-H "Authorization: Bearer $YOPRINT_API_KEY" \
-H "Accept: application/json" \
-H "X-Yoprint-Decimal-Format: exact-v1"

Shortened, for this guide’s order with no tax:

{
"status": 200,
"success": true,
"data": {
"id": "V4keXwwxMQyXw7",
"object_type": "sales_order",
"order_item_cart_total": { "value": 3000000, "dp": 4 },
"invoiced_total": { "value": 3000000, "dp": 4 },
"paid_amount": 300000,
"order_balance_due": { "value": 0, "dp": 4 }
}
}
  • order_item_cart_total is what the order’s lines are worth: 300.00.
  • invoiced_total is what has been invoiced: 300.00.
  • paid_amount is what’s been paid, in thousandths: 300.00.
  • order_balance_due is what’s left to pay, invoiced_total minus paid_amount: 0.

The totals are decimal pairs: read dp every time. If your team adds tax to invoices, your totals are higher and some balance is left. See Money and quantities and How the balance works.

Delete in this order. Deleting a published order moves its payments to the customer’s credit, so delete the payment first.

  1. The payment:

    DELETE Delete a payment or refund /v2/api/store/{team_slug}/v2/sales_order/{hash_id}/payment/{payment_reference}
    Terminal window
    curl -X DELETE "https://secure.yoprint.com/v2/api/store/$YOPRINT_TEAM/v2/sales_order/$ORDER_ID/payment/$PAYMENT_ID" \
    -H "Authorization: Bearer $YOPRINT_API_KEY" \
    -H "Accept: application/json" \
    -H "X-Yoprint-Decimal-Format: exact-v1"
  2. The sales order. This also deletes its invoice and fires order.deleted. It goes to the trash, where it can be restored.

    DELETE Delete a sales order or quote /v2/api/store/{team_slug}/v2/sales_order/{order_unique_key}
    Terminal window
    curl -X DELETE "https://secure.yoprint.com/v2/api/store/$YOPRINT_TEAM/v2/sales_order/$ORDER_ID" \
    -H "Authorization: Bearer $YOPRINT_API_KEY" \
    -H "Accept: application/json" \
    -H "X-Yoprint-Decimal-Format: exact-v1"

Both answer { "status": 200, "success": true, "data": null }. See Test your integration for the customer.

Dates, status, payment terms and other changes

Most other changes to a quote or order go through one operation, with the action’s name in the path:

PUT Run an action on a sales order or quote /v2/api/store/{team_slug}/v2/sales_order/{order_unique_key}/action/{action_name}

Actions include changing dates (change_issue_date, change_customer_due_date, change_start_end_date_date, change_ship_due_date, change_deposit_due, change_full_amount_due), change_status, change_payment_term, change_pricing_group, mark_order_as_rush, clone_order, and the two conversions. change_customer works only on drafts. change_status and the sending actions work only after publishing.

  • Reading the line’s id from data.id. The add-line answer wraps the line in item: its id is data.item.id. data.id doesn’t exist there.
  • Trusting the 200 from publish. Publish can “succeed” without publishing. Always check success and data.valid.
  • A wrong customer_id. It doesn’t fail the create. It makes a draft without a customer, and publishing fails later.
  • Sending a payment as a decimal pair. A payment amount is thousandths (300000 is 300.00).
  • Expecting a copy when you convert. The quote becomes the sales order, with the same id. Use clone_order for a copy.
  • Reading calc_total. It’s deprecated and stays 0 until the order is invoiced. Read order_item_cart_total and invoiced_total.
  • Creating an invoice on an Auto Invoice order. YoPrint refuses it.
  • Retrying a create that timed out. Creates aren’t idempotent: a retry makes a second order, line or payment. Read back first. See Retries and duplicates.