Count only the orders that committed

A mutation schedules the delivery instead of sending it, so an order that rolled back never reaches your numbers.

Package @mirafive/sdk-convex

Set up in 5 steps

  1. Install the packages

    Delivery goes through @mirafive/sdk-server. It needs convex 1.25 or newer and runs in Convex's default runtime, so you add no "use node".

    Code for step 1 of the Convex setup: Install the packages. npm install @mirafive/sdk-convex @mirafive/sdk-server

  2. Set the secret key on the deployment

    Use the secret key of a server source from Data → Sources. It lives in the deployment's environment, never in code or a client bundle. A missing key never breaks the deployment; events are dropped with one warning.

    Code for step 2 of the Convex setup: Set the secret key on the deployment. npx convex env set MIRAFIVE_SECRET_KEY mf_…

  3. Create the client

    Export the client and its delivery action from one module. Keep the : MiraConvex annotation; without it TypeScript reports TS7022.

    Code for step 3 of the Convex setup: Create the client. import { MiraConvex } from '@mirafive/sdk-convex' import { internal } from './_generated/api' export const mira: MiraConvex = new MiraConvex({ key: process.env.MIRAFIVE_SECRET_KEY, deliver: internal.mirafive.deliver, }) export const deliver = mira.deliverAction()

  4. Track the paid order in the mutation

    track() schedules deliver, and Convex runs it only if the mutation commits. The idempotency key makes a mutation that runs twice for the same order count once. await every call.

    Code for step 4 of the Convex setup: Track the paid order in the mutation. import { v } from 'convex/values' import { mutation } from './_generated/server' import { mira } from './mirafive' export const pay = mutation({ args: { orderId: v.id('orders') }, handler: async (ctx, { orderId }) => { const order = await ctx.db.get(orderId) if (!order) { throw new Error('order not found') } await ctx.db.patch(orderId, { paidAt: Date.now() }) await mira.track(ctx, 'order_paid', { userId: order.customerId, properties: { revenue: 328, currency: 'EUR' }, idempotencyKey: `order-${orderId}`, }) }, })

  5. Check the delivery

    Run the delivery action once with an install check, which is never stored or billed. It prints null when MIRA FIVE accepted the batch. Then run a mutation that tracks and find the event under Data → Live; the Convex dashboard shows the mirafive:deliver run.

    Code for step 5 of the Convex setup: Check the delivery. npx convex run mirafive:deliver '{"events":"[{\"name\":\"$install_check\"}]","idempotencyKey":"install-check"}'

MIRA FIVE is analytics built around the person. @mirafive/sdk-convex records events from Convex mutations and actions and delivers them through @mirafive/sdk-server, so a purchase is counted where your app decides the order is paid, and only if that decision is stored.

How does a paid order reach its channel?

Through the person who paid. The browser side of your app uses the browser SDK or your framework’s package, such as React. With consent, the browser keeps an anonymous id; pass it to a mutation, and identify links the visitor’s earlier events to your user id:

await mira.identify(ctx, userId, { plan: 'pro' }, { anonymousId })

Use your internal user id, never an email address. The paid order carries the same userId, so MIRA FIVE puts it on that person, next to the channel that first brought them. Create a purchase goal for order_paid to see buyers and revenue per channel and campaign.

How are repeats counted?

Every delivery carries an idempotency key, fixed when it is queued: yours, or a fresh UUID. A rerun of the action sends the byte-identical batch, and MIRA FIVE stores it once. Pass your own key when a mutation can run twice for the same thing, such as a webhook.

Several events from one mutation go out as one delivery with trackMany():

await mira.trackMany(ctx, [
  { name: 'order_paid', userId, properties: { revenue: 328, currency: 'EUR' } },
  { name: 'invoice_created', userId, properties: { invoiceId } },
])

Server events are sent in full mode by default, and your app holds the consent or other lawful basis for the ids. With mode: 'consentless', events carry no identifiers.

The source’s Install tab has Let your coding agent install it, a prompt for Claude Code, Cursor or Codex with every step and the check; for a server source it never contains the secret key. The Convex SDK docs cover the outbox and workpool retries, and the setup page shows who does what, in order.

Other setups

The same numbers, whichever way your site is built.

Questions and answers

Can a rolled-back mutation count an order?
No. A mutation cannot reach the network, so track() schedules the delivery, and Convex runs a scheduled function only if the mutation commits.
When should I turn on the outbox?
Above roughly one event per second. With outbox on, mutations write events to a miraOutbox table and a cron sends them in batches of up to 1,000. That saves Convex action time, not MIRA FIVE usage.
Does the scheduler retry a failed delivery?
No. Hand deliveries to a Convex workpool through the enqueue option. The idempotency key is fixed when a delivery is queued, so a retried delivery is stored once.
Can I track from a query?
No, queries cannot schedule. track() works in mutations and actions, and in an action it schedules a delivery as well.

See which channel brings buyers

Free for 25,000 events a month. No card needed.