Skip to main content

Connected Platforms — mirror Vora orders into your CRM

Take payment through Vora and keep your existing commerce platform as your system‑of‑record. (Running a WooCommerce store? Use the Von Payments plugin instead: it takes the payment inside your WooCommerce checkout, with nothing to mirror.) When Vora captures a payment, it creates the matching already‑paid order in your connected platform, so orders, fulfillment and customer records stay where they are today.

How it works​

  1. Connect your store once, from the Vora dashboard → Vora → Integrations → Connected Platforms.
  2. Describe the order once, with an order object (what was bought) and a mirrorTo object (which store records it). Vora uses that one description for the checkout page the buyer sees and for the order it creates in your store.
  3. When the payment is captured, Vora creates the already‑paid order in your platform. Vora charged the card; the platform records the order without touching a card.

Describing the order​

{
"amount": 4498,
"currency": "USD",
"order": {
"lineItems": [
{ "name": "Trail Runner - Size 10", "quantity": 1, "unitAmount": 3499, "sku": "TR-10" },
{ "name": "Merino Socks", "quantity": 2, "unitAmount": 200, "sku": "MS-01" },
{ "name": "Standard shipping", "quantity": 1, "unitAmount": 599 }
]
},
"mirrorTo": { "platform": "shopify", "store": "acme.myshopify.com" },
"buyer": {
"email": "casey@example.com",
"name": "Casey Rivera",
"address": {
"addressLine1": "18 Bridge Street", "city": "Portland", "state": "OR",
"postalCode": "97205", "country": "US"
}
}
}

The buyer sees those lines and the shipping row on the checkout page; the store order carries the same lines, the same shipping, and Casey as the customer.

  • Identical on hosted checkout (POST /v1/sessions) and on a direct charge (POST /v1/payment_intents) — same fields, same rules, same errors.
  • order and mirrorTo travel together; one without the other is a 400.
  • Every amount is an integer in minor units — amount, unitAmount, shipping.amount. 3499 is $34.99.
  • The buyer address takes addressLine1 / addressLine2; the store-order addresses (mirrorTo.shippingAddress, mirrorTo.billingAddress) take line1 / line2. Both reject unknown fields, so a mix-up is a 400 naming the field.
  • On most accounts the store order is created after the payment succeeds, out of band: a problem at the store is a missing order you can see and fix, never a failed checkout for the buyer. Accounts that create the order first refuse the charge instead.
  • On a direct charge, order / mirrorTo is validated before the charge: an oversized block, an unconnected store or a missing permission is a 400 / 422 with no money moved.

The money rule​

What you describe must add up to what you charge, in minor units. If the two figures disagree the request is refused with 400 order_total_mismatch before any charge; the response carries derived_amount and charge_amount.

Shopify — shipping is a line item:

Σ (lineItems[].unitAmount × quantity) = amount

Above: 3499 + (200 × 2) + 599 = 4498, with shipping as its own line. order.shipping is refused on Shopify before the charge (400 validation_error, message beginning Invalid order:), because Shopify does not carry that amount onto the order — send shipping as a line item.

Next Commerce — shipping is its own field and counts toward the total:

Σ (lineItems[].unitAmount × quantity) + shipping.amount = amount

order.coupon records a discount code for reference; its amount is display-only and excluded from the total. If a discount changes what the buyer pays, subtract it from amount and the line prices yourself. A store-priced voucher is Next Commerce only, and not expressible on this shape at all — see Three things only the raw block can express.

The buyer is stated once too​

mirrorTo inherits from the buyer you already send: the one buyer address fills both the shipping and billing address on the order. A single buyer.name is split for the store record — the first word is the given name, the rest the family name ("Ada King Lovelace" → "Ada", "King Lovelace"); a one-word name yields a given name only. Set mirrorTo.customer, mirrorTo.shippingAddress or mirrorTo.billingAddress explicitly and your value wins.

Two rules that look like bugs otherwise:

  • Always send city and state on the buyer address. They are optional on the buyer profile but required on a store-order address, and inheritance is all-or-nothing: a buyer address missing either is not inherited at all, and the result is a paid order with nowhere to ship it and no error. If you hold a complete address the buyer record does not, send mirrorTo.shippingAddress.
  • No buyer email means no store customer. The store customer is keyed on email; send mirrorTo.customer yourself.

On a direct charge, the top-level billing_address is not used as a fallback for the store order — it exists for the address-verification check with the payment provider. Only the buyer profile is inherited. Send mirrorTo.shippingAddress if the buyer record has no address.

Fields​

order:

FieldRequiredTypeNotes
lineItemsyesarray (1–100)What was bought. See below.
shippingnoobjectNext Commerce only. amount in minor units (counts toward the total), optional label up to 120 chars. Refused on Shopify — send shipping as a line item, see the money rule.
couponnoobjectcode (1–64 chars) and an optional display-only amount.
metadatanoobjectYour own keys (≤64 chars) and values (≤500).

order.lineItems[]:

FieldRequiredTypeNotes
nameyesstring (1–200)What the buyer sees, and the line description on the store order.
quantityyesinteger (1–9999)
unitAmountyesintegerPrice per unit in minor units — 1499 is $14.99.
variantIdnostring or integerCatalog id of the exact buyable variant. Required per line on Next Commerce.
skunostring (1–255)

mirrorTo:

FieldRequiredTypeNotes
platformyes"shopify" or "nextcommerce"
storeyesstring (1–255)The store host exactly — acme.myshopify.com or acme.29next.store.
customernoobjectemail (required within), firstName, lastName. Inherited from the buyer when omitted.
shippingAddressnoobjectInherited from the buyer when omitted.
billingAddressnoobjectInherited from the buyer when omitted.
parentOrderRefnostring (1–64)Append to an existing store order instead of creating one. Next Commerce only — see below.
upsellKeynostring (1–64)Your key for one upsell decision. Valid only with parentOrderRef.

parentOrderRef is Next Commerce only. A Shopify order can be created but never appended to, so platform: "shopify" with parentOrderRef is refused before the charge with a 400 validation_error whose message begins Invalid order: (the raw mirror block returns mirror_upsell_unsupported for the same case — a handler keyed on that code alone will not match). Send the extra item as its own new order.

An address takes line1, city, state, postalCode and a two-letter country, plus optional name, line2 and phone. Unknown fields are a 400.

Three things only the raw block can express​

A store-priced voucher, the per-charge skip opt-out and attribution are not reachable through order / mirrorTo — sending any of them there returns a 400. For those, send a raw mirror block.

Do not drop a voucher to get past that error

A voucher is a store-priced discount. Removing it so the request validates charges the buyer the discounted amount while the store records a full-price order, and the two never reconcile. Switch to a raw mirror block.

The raw mirror block​

The raw block describes the same store order as order / mirrorTo, and it is the way to reach the three fields above. Send one or the other: a request carrying both is refused with 400 order_mirror_conflict.

Both create calls take the raw block at the top level, as a JSON object (never a string):

  • Hosted checkout (POST /v1/sessions): top-level mirror. A string under metadata.mirror is never parsed; sending both returns 400 mirror_dual_specification.
  • Direct charge (POST /v1/payment_intents): top-level mirror, or the identical object at metadata.mirror (supported, no sunset). Send it in one place or, if identical, in both; two blocks that differ are rejected with 400 mirror_alias_conflict.

The raw block is one shared contract across every destination — the same fields (contract_version, destination, shop, line_items, customer, plus optional skip, attribution, shipping_address, billing_address, metadata). You pick the platform with destination; each platform's page below covers only what's specific to it.

Skipping the store order for one charge​

Set mirror.skip to true to record the payment normally and create no store order for that one charge — for an order you already entered in the store by hand, a test charge against a live store, or a correction. Identical on both create calls; it is a field on the raw mirror block only. skip cannot be combined with a voucher (the store computes a store-priced discount, so there would be nothing recording the store-derived total) — that combination returns a 400.

Supported platforms​

PlatformStatusGuide
ShopifyAvailableShopify order mirroring
Next Commerce (29next)AvailableNext Commerce order mirroring

Attribution and your own metadata​

Pass marketing / direct‑response data (affiliate, utm_*, funnel, and a passthrough map) as attribution on the raw block and it lands on the created order — Next Commerce maps it to the order's native attribution fields, Shopify to order custom attributes. Attach your own correlation data with mirror.metadata, and reference the charge from the order via the vora_payment_intent_id stamped on it. The raw block is capped at 4096 bytes, checked before the charge on hosted checkout and on Next Commerce direct charges.

When the order is created​

The store order is created when the payment is captured, not when it is authorized. On an ordinary automatic capture those are the same moment. On a manual capture (capture_method: "manual"), no store order exists while the charge is only authorized, and the poll route reports pending until you capture (POST /v1/payment_intents/{id}/capture) — it does not advance on its own. On these accounts a mirrored order always represents money already captured; on order-before-charge accounts it does not.

Confirming the order​

A successful charge is not proof the store order exists. Poll the route for the flow you used, or subscribe to the mirror.order.created webhook (it carries payment_intent_id, order_number and order_status_url).

  • Hosted checkout: GET /v1/public/sessions/{sessionId}/mirror-order, with your publishable key (vp_pk_*; a secret key is refused).
  • Direct charge: GET /v1/payment_intents/{id}/mirror-order, with your secret key (vp_sk_*).

Both return { status, order_number, order_status_url }; order_number and order_status_url are non-null only when status is created. order_number is the number the shopper and your store staff see, safe to print on a thank-you page: on Shopify the order name (for example #1001), elsewhere the platform's order number. A Shopify order created before 28 September 2026, or one recovered by an automatic retry, can carry the store's internal numeric id instead. order_status_url is a private link to that shopper's order: show it only to them, and do not log, store or share it.

statusWhat it means
pendingNot created yet. Keep polling, with a cap. On the direct charge this also covers a charge that carried no mirror block at all, which that route cannot tell apart from "not finished yet".
createdThe store order exists.
failedTerminal — stop polling. On the direct charge a code and a plain-language message say why. Read code before you act: most values mean the order was rejected and needs your attention, but one means it was released on purpose and needs nothing. See What code tells you.
not_mirroredTerminal, but read the warning below before you act on it.

What code tells you​

On the direct charge, a failed response carries a stable code and a merchant-safe message. Branch on code.

codeWhat happenedWhat to do
mirror_line_invalidA line on the order was rejected — typically a product reference the store could not resolve.Fix the line and resend.
mirror_connection_errorThe store connection needs re-authorizing.Reconnect the store, then retry.
mirror_shipping_country_unsupportedThe store does not deliver to this order's country using the shipping method that was sent. The connection is healthy.Add that country to the shipping method in the store's settings, or send a shipping method that already covers it.
mirror_rejectedThe store rejected the order for a reason we could not classify.Compare the order's items, shipping method and delivery address against the store's settings. Reconnecting does not help here — a broken connection has its own code.
mirror_order_cancelledNothing is wrong. An order was created before the payment, the payment did not complete, and the order was released.Nothing. No order stands and no money moved.

mirror_order_cancelled only appears on accounts using order-before-charge — see below.

If you receive a code this table does not list, show its message, which says what happened and what to do.

not_mirrored is not proof that no order exists​

order_number is null in every not_mirrored case, so the response cannot tell these apart:

  • the session carried no mirror block at all (hosted route only; the direct-charge route reports pending for that case);
  • the mirror was recorded but never dispatched — the store was disconnected, its authorization was revoked, or mirroring is off for the account. No order exists and the charge needs reconciling;
  • an order was created in the store and later cancelled there.
Check the store before you reconcile or refund

A second refund issued on the strength of not_mirrored is money out twice. Stop polling, open the store, then act.

On order-before-charge accounts, created does not mean the payment succeeded​

Most accounts create the store order after the charge settles, so created implies money moved. Some accounts create the order first: on those, the direct-charge poll can report created for a payment that then fails — created answers "does an order exist?", not "has the payment completed?". Confirm payment from the payment intent's own status or the charge.succeeded webhook. If the payment never completes, the order is released automatically and the same endpoint switches to failed with code: mirror_order_cancelled — no order stands and no money moved. A captured payment always reports created with its order_number, so a settled order never hides behind a failed.

Retrying safely​

Duplicate protection is per payment, not per cart: at most one store order per payment.

  • Retry with the same Idempotency-Key → one charge, one store order.
  • Retry the same cart with a fresh Idempotency-Key, or as a new payment → a second charge and a second store order.

An order that lands in the store with fewer lines than you sent is never re-driven (re-driving could create a duplicate order); the poll still reports created, so compare the mirrored order's lines against what you sent if that matters to you.

Testing: use reserved email addresses​

When you test, give the store customer an email at a reserved test domain: any address at example.com, example.org, example.net or @localhost, or one ending in .test, .invalid or .localhost. A realistic address is refused before the charge with mirror_sandbox_email_not_reserved, on every store platform. The check runs on POST /v1/sessions and POST /v1/payment_intents, for a test key or any request from a sandbox account.

Get started​

  1. Connect your store in the dashboard — each platform's guide has the exact steps: Shopify, Next Commerce (29next).
  2. Send order and mirrorTo on your create call and confirm the order appears in your store.