Skip to main content

Mirror a Vora order into Next Commerce (29next)

Send a mirror block with any Vora payment and, on a successful charge, Vora creates the matching already-paid order in your Next Commerce (29next) store. Vora charges the card; 29next stays your system-of-record for fulfillment and CRM. Post-purchase upsells consolidate onto the same order (one order, multiple payments).

Before you send a mirror block

  1. Connect each 29next store in your Vora dashboard → Vora → Integrations → Connected Platforms → Connect 29next. You'll paste the store host (your-store.29next.store) and a 29next Admin API token (Settings → API Access → Create App → grant Orders read/write → copy the token). You can connect more than one store — repeat this per store (see Multiple stores).
  2. Create an External Payment Method in 29next (Settings → Payments → External Payment Methods → Add): name it anything, and set its Code (we recommend vora). You enter this same code when you connect the store, so it matches exactly — the code is per store. (If Code is left blank, 29next auto‑generates one; set it explicitly so you know what to enter.)

Where the mirror block goes

Add mirror as a top-level field on your create call — not nested under metadata:

POST /v1/sessions            // or POST /v1/payment_intents
{
"amount": 2900,
"currency": "USD",
"mirror": { "...": "..." }
}

Validated strictly — any unknown key returns validation_unknown_field, and the serialized block must be ≤ 4096 bytes (mirror_too_large). To skip mirroring on a charge, omit mirror.

The mirror block — field reference

FieldRequiredType / limitNext Commerce notes
contract_versionstring (1–10)"2026-05-15".
destination"nextcommerce"Selects the 29next adapter.
shopstringYour store host, e.g. "your-store.29next.store" — also selects which connected store to mirror to.
line_itemsarray 1–100{ title, quantity, price, external_product_ref }.
customer✅*objectemail (required), first_name, last_name. Not required on an upsell.
shipping_addressoptionalobjectNeutral address. Required for physical orders.
billing_addressoptionalobjectSame shape; defaults to shipping.
shippingoptionalobject{ code?, price? } — the order's shipping method code + amount. Omit → the store's default. See Shipping method + amount.
attributionoptionalobjectMarketing / direct-response — maps to order.attribution.
metadataoptionalRecord<string,string>Your own correlation data, carried onto the 29next order.
parent_order_reffor upsellsstringThe initial order's number. Present → this charge is an upsell appended to that order; absent → create a new order.

Line items → 29next variants

{ "title": "Rad Cat Tee", "quantity": 1, "price": "29.00", "external_product_ref": "12345" }
  • price is a string ("29.00").
  • external_product_ref is required for 29next — it must be the sellable (variant) id: the id of the specific buyable variant (size / colour / option), a positive integer as a string. It maps to the order line's product_id.
  • Do not send the parent product id. A parent product has no price of its own, so 29next rejects the whole order — the charge still succeeds, but the mirror-order comes back failed. 29next resolves the parent from the sellable id automatically; there is no separate variant or package field to send.
  • A line with no ref at all is rejected up front as mirror_line_missing_product_ref.
  • Finding a variant id: in 29next, call GET /api/admin/products/{product_id}/ and use each entry in variants[].id. (Even a single-variant product has its own variant id — use that, not the product id.)

Address

{ "name": "Sam Rivera", "line1": "1 Test St", "line2": "Apt 4",
"city": "Austin", "state": "TX", "postal_code": "78701", "country": "US", "phone": "5125550123" }

line1, city, state, postal_code, and a 2-letter country are required when present.

Shipping method + amount

"shipping": { "code": "express", "price": "12.50" }

Optional. code is your store-configured shipping method code (e.g. default / express); price is a non-negative decimal string. Omit the block (or a field) and 29next applies its own default method and price (the default method). Set these so your store's shipping reporting matches what Vora charged.

Attribution → native 29next order fields

"attribution": {
"affiliate": "acme", "funnel": "spring-promo",
"utm_source": "newsletter", "utm_campaign": "spring",
"passthrough": { "affid2": "xyz", "landing_page": "/lp/spring" }
}

Named fields (affiliate, subaffiliate1-5, funnel, utm_*) map 1:1 to order.attribution.*; anything under passthrough (≤ 255 chars each) lands in order.attribution.metadata.

Multiple stores

A single Von Payments merchant can connect several 29next stores and route each transaction to the right one.

Connecting. Repeat the Connect step per store, each with its own Admin API token and External Payment Method code. Per-store tokens give hard cross-brand isolation — a Store B token can't touch Store A.

Routing. There's no separate routing rule — the mirror.shop field on each mirror block is the router. Set it to the destination store's host for that transaction:

"mirror": { "destination": "nextcommerce", "shop": "brand-a.29next.store", ... }   // → Store A
"mirror": { "destination": "nextcommerce", "shop": "brand-b.29next.store", ... } // → Store B

The connector loads that store's credentials for the order push, settle, refund, and inbound-webhook verification — so orders land in, and refunds/webhooks resolve against, the store named in shop.

Connect first. A mirror.shop that isn't a connected, active store for your account returns 400 mirror_shop_not_authorized at session-create, before any charge — connect the store first.

What happens on a successful charge

  1. Vora charges the card.
  2. The rail creates the order in 29next as payment_method: "external" (the External Payment Method you configured), then settles it to paid — no card is charged in 29next.
  3. The order appears in your 29next admin with your line items, customer, address, and attribution.

Mirroring is idempotent — a retry never creates a duplicate order.

Getting the order back (webhook + retrieve)

The 29next order is created asynchronously — a few seconds after the charge succeeds — so the order number isn't in the charge response. Get it back either way:

Webhook (recommended). Subscribe to the mirror.order.created event; Vora pushes it when the order is created:

{
"payment_intent_id": "vpi_...",
"order_number": "10023",
"order_status_url": "https://your-store.29next.store/..."
}

Retrieve (poll). Poll the mirror-order endpoint for the charge until the order lands — one route per flow, both returning the same shape:

// Hosted / session flow — publishable key
GET /v1/public/sessions/{sessionId}/mirror-order

// Discrete payment-intent flow — secret key
GET /v1/payment_intents/{id}/mirror-order

→ { status, order_number, order_status_url, code?, message? }
status ∈ "pending" | "created" | "failed" | "not_mirrored"
  • pending — not created yet, or a retriable hiccup — keep polling.
  • createdorder_number (+ order_status_url when 29next provides one) are present.
  • failed — a terminal rejection: the order will not be created no matter how long you poll (e.g. an external_product_ref that points at a parent product instead of a variant). The response adds a self-healing code and a merchant-safe message — fix the input and resend rather than polling forever.
  • not_mirrored — the charge carried no mirror block.

Use order_number for your thank-you page and as parent_order_ref when sending upsells. order_status_url is 29next's signed customer order page.

Post-purchase upsells (one order, multiple payments)

A post-purchase upsell is just another Vora charge — there's no separate upsell API. To append an accepted upsell to the buyer's existing 29next order (instead of creating a second order), send the upsell charge with mirror.parent_order_ref set to the number of the initial order:

POST /v1/payment_intents        // (or /v1/sessions) — the upsell charge
{
"amount": 1500,
"currency": "USD",
"mirror": {
"contract_version": "2026-05-15",
"destination": "nextcommerce",
"shop": "your-store.29next.store",
"parent_order_ref": "10023",
"line_items": [
{ "title": "Warranty add-on", "quantity": 1, "price": "15.00", "external_product_ref": "67890" }
]
}
}
  • parent_order_ref makes this an append: Vora adds the upsell line to that order and records the payment as a second external transaction — one 29next order with N payments.
  • Exactly one line_item per upsell. An upsell charge must carry a single line. To sell several upsell products, send each as its own charge with its own parent_order_ref append.
  • customer and shipping_address are not required on an upsell (inherited from the parent).
  • Omit parent_order_ref (default) to create a new order.
  • The reference is validated against your account + store — an upsell to an order you don't own, or on a different store, is rejected.

Correlating the order back to Vora

Every mirrored order carries a von_payment_intent_id in its 29next order.metadata so you can find, reconcile, or support it from inside 29next; read it off the order, then resolve the full Vora record (including your complete metadata) via GET /v1/payment_intents/{id}.

Attaching your own metadata

  • payment_intents.metadata (open JSON, ≤ 8KB) is retained on the Vora payment intent and readable via GET /v1/payment_intents/{id}.
  • mirror.metadata (neutral Record<string,string>) is carried onto the 29next order itself, so your correlation data is visible directly in the CRM.

Complete example (initial checkout)

{
"amount": 2900,
"currency": "USD",
"metadata": { "your_order_id": "A-1001" },
"mirror": {
"contract_version": "2026-05-15",
"destination": "nextcommerce",
"shop": "example-store.29next.store",
"line_items": [
{ "title": "Rad Cat Tee", "quantity": 1, "price": "29.00", "external_product_ref": "12345" }
],
"customer": { "email": "buyer@example.com", "first_name": "Sam", "last_name": "Rivera" },
"shipping_address": {
"line1": "1 Test St", "city": "Austin", "state": "TX", "postal_code": "78701", "country": "US"
},
"attribution": { "utm_source": "newsletter", "utm_campaign": "spring" },
"metadata": { "your_order_id": "A-1001" }
}
}

End-to-end test checklist

  1. Connect your 29next store and confirm the wizard shows connected.
  2. Grab a real 29next variant id (use as external_product_ref).
  3. Fire the initial charge with a Vora test card; complete the returned checkout_url.
  4. Catch the mirror.order.created webhook (or poll the retrieve) → note the order_number.
  5. In 29next → Orders, confirm a new paid order with the right line items, customer, address, attribution — and the von_payment_intent_id in the order metadata.
  6. Send an upsell charge with parent_order_ref = that order_number; confirm the same order now has a second line + a second payment.

Errors

ResponseMeaningFix
validation_unknown_fieldUnexpected key in mirrorSend only the documented fields.
mirror_too_largeBlock > 4096 bytesTrim line items / metadata.
mirror_line_missing_product_refA line has no external_product_refAdd the 29next variant id to each line.
mirror_shop_not_authorizedshop isn't a connected, active store for your accountReconnect; confirm shop = your *.29next.store host.
mirror_dual_specificationSent both top-level mirror and legacy metadata.mirrorSend mirror at the top level only.