Shopify order mirroring
If you've connected a Shopify store to Von Payments, you can have a successful
hosted checkout mirror the order onto that store automatically — a Shopify
order is created for you as soon as the payment completes. You opt in per
checkout by attaching a mirror block to the session you create.
This page is the reference for that block: where it goes, every field it accepts, a full worked example, and the errors you can get back.
Before you start
The store you name in mirror.shop must already be connected to your Von
Payments account. Connect it in the dashboard under Connected Platforms →
Shopify. If you send a mirror.shop that isn't a connected, active store, the
session-create call is rejected with mirror_shop_not_authorized —
this is a deliberate guard so no merchant can drive orders into a store they
don't own.
Where the block goes
mirror is an optional top-level field on the create-session request
(POST /v1/sessions) — the same call you already make to start a hosted
checkout. It is not a separate endpoint. When the checkout completes, the order
is mirrored to the connected store; when you omit mirror, nothing is mirrored.
The create-session request is strict: it rejects unknown top-level fields, and
so does the mirror block itself. Only the fields documented below are
accepted — a typo or an extra field returns validation_unknown_field
rather than being silently dropped.
A hosted-checkout session has its own optional lineItems (used for the
checkout display, priced in minor units). mirror.line_items is a separate
array that describes the Shopify order (priced as decimal strings, Shopify's
convention). They serve different systems — fill in whichever each side needs.
Field reference
The mirror block requires all five fields below. There are no optional
top-level fields on the block today.
| Field | Required | Type | Notes |
|---|---|---|---|
contract_version | yes | string | Block-shape version. Use "2026-05-15". |
destination | yes | string | The connected platform. Only "shopify" is supported today. |
shop | yes | string | Your store's .myshopify.com hostname (e.g. your-store.myshopify.com). Must be a store you've connected. |
line_items | yes | array (1–100) | The order lines mirrored to Shopify. Each item: see below. |
customer | yes | object | The customer recorded on the Shopify order. See below. |
line_items[]
Each entry describes one line on the Shopify order. Between 1 and 100 items.
| Field | Required | Type | Notes |
|---|---|---|---|
title | yes | string (1–255) | Product/line name. |
quantity | yes | integer (1–9999) | Units ordered. |
price | yes | string (≤50) | Unit price as a decimal string in your store currency, e.g. "14.99". Not minor units. |
You don't need to reconcile the line-item total against the charge amount — a
divergence (tax, fees, discounts, partial mirrors) is logged but not
rejected. The charge amount on the session is what Von Payments captures;
mirror.line_items is what appears on the Shopify order.
customer
| Field | Required | Type | Notes |
|---|---|---|---|
email | yes | string (email, ≤254) | The buyer's email; recorded on the Shopify order. |
first_name | no | string (≤100) | |
last_name | no | string (≤100) |
Only these three fields are accepted — there is no phone or address field on
mirror.customer. Shipping and billing addresses are collected by Shopify's own
order flow, not through this block.
Worked example
A create-session request that charges $29.98 and mirrors a two-unit order to
the connected store:
curl https://checkout.vonpay.com/v1/sessions \
-H "Authorization: Bearer vp_sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"amount": 2998,
"currency": "USD",
"successUrl": "https://your-store.example.com/thanks",
"cancelUrl": "https://your-store.example.com/cart",
"mirror": {
"contract_version": "2026-05-15",
"destination": "shopify",
"shop": "your-store.myshopify.com",
"line_items": [
{ "title": "Premium Widget", "quantity": 2, "price": "14.99" }
],
"customer": {
"email": "buyer@example.com",
"first_name": "Ada",
"last_name": "Lovelace"
}
}
}'
The response is a normal session object (checkoutUrl, expiresAt, …) — the
mirror block doesn't change the session response shape. The Shopify order is
created after the buyer completes payment, not at session-create time.
After the checkout completes
- On success the order is created on the connected Shopify store. Track it in the dashboard under Connected Platforms → Shopify.
- On a transient failure (a brief Shopify-side error) the mirror is retried automatically for a few hours before being parked for manual review. The original payment is unaffected — a failed mirror never reverses a completed charge.
- Inspect mirror status, and re-run a parked order, from the Connected Platforms → Shopify dashboard.
Size limit
The serialized mirror block is capped at 4096 bytes. Over the cap returns
mirror_too_large. Keep line-item titles short and put long free-text
elsewhere; the block only needs what Shopify records on the order.
Sandbox
When you're testing with sandbox (test-mode) keys, use a placeholder email such
as buyer@example.com in mirror.customer.email. A completed sandbox checkout
still records that address on the resulting test order, so keep real buyer emails
out of your test runs.
Migrating from metadata.mirror
An earlier integration path put the mirror instructions in a stringified
metadata.mirror value. That path is deprecated (sunset window opens
2026-08-27). Send the structured top-level mirror field instead. If a request
carries both the top-level mirror and a legacy metadata.mirror string,
it's rejected with mirror_dual_specification — pick one, and it
should be the top-level mirror.
Errors
All of these are 400 responses on POST /v1/sessions.
| Code | When | Fix |
|---|---|---|
mirror_shop_not_authorized | mirror.shop isn't a connected, active store for your account | Connect the shop under Connected Platforms → Shopify, then retry with the exact hostname. |
mirror_dual_specification | The request sends both top-level mirror and a legacy metadata.mirror string | Remove metadata.mirror; keep the top-level mirror. |
mirror_too_large | The serialized mirror block exceeds 4096 bytes | Trim line-item titles / move long free-text out of the block. |
validation_unknown_field | The block (or the request) contains a field that isn't in this reference | Remove the unrecognized field — only the documented fields are accepted. |
What this block is not
To keep integrations accurate, note what the deployed contract does not
accept today. Sending any of these returns validation_unknown_field (the block
is strict):
- No per-transaction
skip,tags,note_attributes,send_receipt,financial_status, ororiginon the block. - No
shipping_address/billing_addresson the block (Shopify collects those in its own order flow). - No
variant_idor tax fields on a line item, and nophoneon the customer.
If you need one of these, check back — the block is versioned
(contract_version) precisely so it can grow without breaking existing
integrations.