Skip to main content

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.

The mirror line items are separate from the session's line items

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.

FieldRequiredTypeNotes
contract_versionyesstringBlock-shape version. Use "2026-05-15".
destinationyesstringThe connected platform. Only "shopify" is supported today.
shopyesstringYour store's .myshopify.com hostname (e.g. your-store.myshopify.com). Must be a store you've connected.
line_itemsyesarray (1–100)The order lines mirrored to Shopify. Each item: see below.
customeryesobjectThe customer recorded on the Shopify order. See below.

line_items[]

Each entry describes one line on the Shopify order. Between 1 and 100 items.

FieldRequiredTypeNotes
titleyesstring (1–255)Product/line name.
quantityyesinteger (1–9999)Units ordered.
priceyesstring (≤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

FieldRequiredTypeNotes
emailyesstring (email, ≤254)The buyer's email; recorded on the Shopify order.
first_namenostring (≤100)
last_namenostring (≤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.

CodeWhenFix
mirror_shop_not_authorizedmirror.shop isn't a connected, active store for your accountConnect the shop under Connected Platforms → Shopify, then retry with the exact hostname.
mirror_dual_specificationThe request sends both top-level mirror and a legacy metadata.mirror stringRemove metadata.mirror; keep the top-level mirror.
mirror_too_largeThe serialized mirror block exceeds 4096 bytesTrim line-item titles / move long free-text out of the block.
validation_unknown_fieldThe block (or the request) contains a field that isn't in this referenceRemove 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, or origin on the block.
  • No shipping_address / billing_address on the block (Shopify collects those in its own order flow).
  • No variant_id or tax fields on a line item, and no phone on 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.