Tracking works wherever your app runs. Only the overage charge that may follow needs your app hosted on Meridian. Without that the usage row is still recorded and
track still succeeds. The charge is simply never raised.What to send
Report the action, not the money. The shop’s plan decides what is included and what is billed, so one
track call bills differently on different plans, and on a plan that doesn’t meter the event at all, it just records.
quantity counts, properties measure
quantity is a batching multiplier: how many whole units of the event this one call reports. One order is quantity: 1. One call reporting fifty sent emails is quantity: 50. It is a whole number.
Everything measured (an amount, a weight, a byte count, a duration) goes in properties as a numeric property:
total_price, and a plan can price either. An amount squeezed into quantity can never be separated back out.
A key that isn’t in your catalog is accepted and stored anyway, so track never fails on a metric you haven’t defined yet. It shows up in Activity flagged as not-in-catalog, with a one-click way to declare it.
The key is the only identifier track takes. An event’s uuid is not an alias for it: a call that sends one is refused with a 422 (VALIDATION_ERROR), so the mistake surfaces in your logs instead of as a metric nobody declared.
Send real properties
properties is stored verbatim and never interpreted, which makes it the one part of a track call you cannot backfill. Send the ids and the numbers you might want to aggregate later:
Idempotency
Pass anidempotencyKey when a retry could double-count: a webhook handler, a queued job, anything that might run twice. Meridian keys it per app and shop, so a second call with the same key returns the original event and charges nothing.
The key also changes how the SDK behaves on a flaky network: a track call with an idempotency key is retried on 429 and 5xx, and one without it is sent exactly once. Where accuracy matters, send a key.
Sending events in batch
Anything that runs on a schedule produces its events in a burst: an hourly order sync, a nightly reconciliation, a queue worker draining a backlog. Send them as one batch withtrackMany rather than one track call per event.
createMeridianApp, keyed by shop domain rather than token. It is best-effort like the rest of that surface: null when Meridian is unreachable, never an exception in your job.
track:
null when unconfigured, identify fails, tracking fails, or the rate-limit breaker is open. See Tracking usage events for the failure and retry contract.
A batch beats a Promise.all of track calls for three reasons. It is one round trip instead of N. It counts as one request against your rate limit instead of N. And Meridian records it in a single database transaction, so the events cannot deadlock against each other, which is exactly what N near-simultaneous writes for the same shop can do.
What a batch does
Every event in the batch lands in one transaction. Either all of them are recorded or none is, and the counters and views that read them move once, by the sum. Automations still see each event:usage_event.tracked fires once per event recorded, and a threshold trigger is evaluated once for the batch as a whole, on where the store stood before it and after it. Events dated more than 24 hours back are the exception: they fire neither. One transition carries one event, and a crossing names the exact one: the event of the batch at which the store’s running total reached that automation’s threshold.
Idempotency is per event. An event whose key was already recorded is answered with the stored event and created: false, contributes nothing, and does not stop the others. A retry of the whole batch is therefore safe when every event carries a key, and that is also the rule the SDK follows: trackMany is retried on 429 and 5xx only when every event has an idempotencyKey, and sent exactly once otherwise.
Validation is all or nothing on shape. An event uuid in place of a key anywhere in the batch, or two events sharing an idempotency key, is a 422 for the whole request and nothing is recorded. An undeclared key is still accepted and counted, as it is for a single event.
A batch holds at most 50 events. That cap is the trade-off for counting as one request: a bigger batch would hold the store’s counters locked for longer than a request should.
The HTTP shape
trackMany posts to the same endpoint as track, with the events wrapped in an array:
id and timestamp are the stored event’s, not this request’s.
Dated events
An event is dated the moment Meridian receives it. When your app reports usage late, pass atimestamp with the moment it actually happened, so the event lands on the right day, in the right charts and in the right billing period. Two cases call for it:
- A sync that runs behind. An hourly order sync that was down for a morning catches up with orders placed hours ago.
- A replay after an incident. Your queue or your logs hold the events a failed deploy never reported, and you send them now.
track takes a timestamp too, on the server client and on createMeridianApp. The useMeridian() hook does not: backdating is a job for your server, not for the browser.
The rules
Billing follows the event’s own period
A dated event is counted and billed according to the period it happened in, never the one it arrived in. The period is the one described in Which period: the store’s 30-day billing cycle when it is subscribed, the UTC calendar month otherwise.- Dated inside the store’s current period: it counts exactly like a live event. It moves
usedThisPeriodand the plan’s counters, and it can raise an overage charge, however many days ago it happened. - Dated before the current period started: it is history. It is stored, and it shows in Activity, in the charts and in every view whose window includes its date (an all-time view always does). It moves no counter and is never charged, so replaying last year’s orders never bills a merchant today.
An event dated just before a period closed and received just after it is never billed. Take an order placed at 09:50 in a period that rolled over at 10:00, reported at 10:20: its period is closed, so it is recorded but counted in neither period. It only costs you anything if the store had already gone past its included quantity in the period that closed.
History from before Meridian
Moving an existing app to Meridian usually means bringing its past with it: months or years of orders that happened before the app existed here. That history is free of your app’s own Meridian allowances during the app’s first 30 days, so a migration never spends the month and leaves your live events refused. An event uses neither the app’s monthly usage events nor its SDK requests when all of this holds:- It is sent with
trackMany. - Its
timestampis before the moment the app was created on Meridian. - Meridian receives it at most 30 days after the app was created.
track, an event dated after the app was created, and anything sent once the 30 days are over. A trackMany call made only of such events costs no SDK request. A call that mixes them with other events costs one, and only its other events use the usage events allowance.
While the window is open, the Events page and the SDK step of the setup guide show the date it closes.
idempotencyKey on every event (so the script can be re-run safely), at most 5 years back, and installed stores only. Merchant billing is not affected: an old event is counted for the store exactly as Billing follows the event’s own period describes, so one dated inside the store’s current period still counts towards its usage.
Automations fire for recent events only
Only an event dated less than 24 hours before Meridian receives it fires automations: Custom event is tracked and Usage view crosses a threshold. An older one fires nothing, even when it falls in the current period and is counted. It simply becomes part of where the store stood when the next live event arrives, so a replay sends no late “first order” email and no burst of “new order” emails, and the next live event is measured correctly. The cost of that rule: a milestone reached only by events that arrive more than 24 hours late is never sent. A threshold also only counts the events inside the view’s current window. An order dated last month does not move a month-to-date view, so it cannot take one across a threshold, even when it arrives today. See When it fires.A known key keeps its first date
A dated event is idempotent like any other. Sending anidempotencyKey Meridian has already recorded, with a different timestamp, changes nothing: you get the stored event back (with created: false in a batch), carrying the timestamp it was stored with. The first write wins, and the response tells you which date was kept. Meridian never moves an event it has recorded.
Errors
A timestamp that breaks a rule is refused with a422 and the code VALIDATION_ERROR, and nothing in the request is recorded. error.details.fields names the field. In a batch the name carries the event’s position (events.3.timestamp), and one bad event refuses the whole batch.
Date that is invalid (new Date("garbage")) makes the server client’s track and trackMany throw a TypeError without calling Meridian, and makes the createMeridianApp methods resolve null. Sent as JSON, an invalid Date would have become null.
Reading usage back
getUsage answers for every event in your catalog, whether or not the shop’s plan prices it: an app that meters without selling usage reads its own numbers, and so does a store on no plan at all. includedQuantity and cappedAmount appear only where the plan actually sets terms; their absence is how you tell the metric isn’t billed on this plan.
Usage arrives with the rest of the provider’s state, so showing a merchant their remaining allowance costs nothing extra. Call refresh() after a burst of tracking if you need the figure updated in the same session. The snapshot is read at load, not per call.
Which period
usedThisPeriod is a current-period figure. It resets to zero at each period boundary, and the merchant’s included quantity refreshes with it.
entitlements.periodStart and entitlements.periodEnd give you that window, so you can tell a merchant when their allowance refreshes rather than letting them discover it. periodEnd is exclusive: it is the instant the counter resets.
Overage is charged against the same window. A store that ran over its included quantity last cycle starts the next one with a full allowance again.
The account page already renders this as a bar per event, including the over-limit state and the raise-cap flow, so you do not have to.
On your side, Usage → Activity and Usage → By store show the same events as a raw log and a per-store rollup. See Activity and usage by store.
Gating on an allowance
track records usage; it does not refuse. Deciding whether an action is allowed at all is yours:
What a tracked event triggers
The usage row is written before anything else happens, so it survives a failed charge. A billing problem never loses the record of what your app did. If Shopify’s answer to an overage charge is lost, Meridian sends that same charge again, under the same idempotency key, before it bills anything new, so a retry never charges the same usage twice. Tracking also fires theusage_event.tracked automation trigger, so an event can drive an email or a CRM update without further wiring. A dated event more than 24 hours old is recorded and fires nothing. The properties you sent travel with it: each scalar one becomes a properties.<name> variable the flow can branch on and an email can print, under the exact key you used. A failed dispatch is logged and never fails the track call. Usage is also one of the datasets the report builder can read.
A call de-duplicated by its idempotency key fires nothing and charges nothing. Only the first one does.