What custom events are
The Partner API tells AppVitals about installs, subscriptions and payments. Custom events cover everything it can’t: onboarding steps completed, exports finished, feature usage, background jobs, errors, anything your app’s backend can observe. You send them over a small REST API (or the official Node client), and AppVitals treats them as first-class data:
- Every event belongs to one of your apps and can be paired to a concrete store by its myshopify domain, the same key the rest of AppVitals uses
- The Explorer charts any event with count, unique-ID and numeric-property measures, breakdowns and filters, and lets you save the result as a report
- Slack notification rules can fire on chosen events, with a per-event message template you control
Quickstart
Create an API key
In Organization → API access create a key with the
write_custom_eventsscope; addidentify_customersif you also want to push customer profiles (below). Theav_…secret is shown once at creation, store it server-side. API keys must never ship in frontend code.Send your first event with curl
curl -X POST https://appvitals.io/api/custom-events/ingest \ -H "Authorization: Bearer av_..." \ -H "Content-Type: application/json" \ -d '{ "name": "Export finished", "appId": "gid://partners/App/123456", "myshopifyDomain": "shop.myshopify.com", "insertId": "export-1024", "properties": { "format": "csv", "rows": 1200 } }'A
200response reports{"inserted": 1, "duplicates": 0, "blocked": 0, "failed": []}plus your month-to-date usage. The event appears in the Custom events pages immediately.Or use the official client
The client is published on npm: npmjs.com/package/@appvitals/client.
npm install @appvitals/clientimport { AppVitalsClient } from "@appvitals/client"; const appvitals = new AppVitalsClient({ apiKey: process.env.APPVITALS_API_KEY!, // av_... }); // One event; insertId makes retries safe. await appvitals.sendCustomEvent({ name: "Export finished", appId: "123456", // Partner gid or bare numeric id myshopifyDomain: "shop.myshopify.com", insertId: "export-1024", properties: { format: "csv", rows: 1200 }, }); // Or a batch (max 100 per request). await appvitals.sendCustomEvents([...]);The client is server-side Node (18+). By default a failed call is logged with
console.warnand resolves tonullinstead of rejecting, so fire-and-forget withvoid appvitals.sendCustomEvent(…)can never take your app down; opt into rejections withonError: "throw". To test against a dev or staging instance, set theAPPVITALS_BASE_URLenvironment variable (or thebaseUrloption) to its origin.
Ingest reference
POST /api/custom-events/ingest with Authorization: Bearer av_… (scope write_custom_events). The body is one event object, or {"events": [...]} with up to 100 events per request.
| Field | Meaning |
|---|---|
name | Required. 1 to 120 characters, starting with a letter or digit; letters, digits, spaces and _ - . : / allowed. Keep one consistent spelling per event. |
appId | Required. Your app's Partner gid (gid://partners/App/123456) or its bare numeric id. Must be one of your organization's apps. |
myshopifyDomain | Optional. Pairs the event to a concrete store, the same key identify uses. When distinctId is omitted, the domain doubles as the identity. |
distinctId | Optional. A custom identity (user id, session id, ...) powering the Unique IDs measure. A distinctId that is itself a *.myshopify.com domain pairs the event to that store automatically. Omit both identity fields for app-level events. |
time | Optional. ISO 8601 string or unix milliseconds; defaults to arrival time. Up to 5 years in the past, at most 10 minutes of future clock skew. |
insertId | Optional idempotency key, 1 to 64 characters of letters, digits, _ or -. Events with the same (appId, name, distinctId, insertId) are stored once; resends count as duplicates. Without it every send creates a new event. |
properties | Optional. Up to 50 flat scalar values (string ≤ 500 chars, number, boolean, null); no nested objects or arrays. Keys are 1 to 60 characters of letters, digits, _ - or . — use dotted keys like plan.name for hierarchy; every property stays usable as a chart breakdown and filter. |
Batches and partial acceptance
Batches are accepted item by item: valid events are stored, invalid ones come back in failed[] as {index, error} records aligned with your request array. An unknown appId is a per-item error too. The request only answers 400 when the envelope is malformed or no event in it was accepted. When a batch would exceed your monthly allowance without usage billing, it is accepted up to the limit and the rest fails with Monthly included event limit reached - enable usage billing to continue (see limits below).
Responses and errors
200withinserted,duplicates(insertId resends),blocked(event names you blocked in the catalog),failed[]and ausagesnapshot400malformed body, empty batch, more than 100 events, or every event invalid401missing or invalid API key,403key lacks thewrite_custom_eventsscope
The complete schema with runnable examples is in the REST API reference.
Identify: customer profiles, tags & contacts
POST /api/customers/identify (scope identify_customers; client method appvitals.identify(…)) is the companion endpoint your backend calls on install and on every profile change. One payload, keyed by myshopifyDomain + appId, updates three things at once, all partially: omitted fields keep their stored values.
- Customer tags:
tagsapply to the store itself (matched by its myshopify domain, no email needed), the same tags you edit in the Customers table.operators.tagscontrols the merge:replace(default) swaps the whole list,upsertadds missing tags and keeps the rest (appendis an alias),removedeletes just the sent tags - Store profile & custom attributes: fields like
primaryLocale,shopifyPlan,currency,timezone,createdAtandstorefrontDomaincarry facts from your own Admin API that the Partner feed never exposes;customFieldsholds app-scoped attributes merged per key: flat scalars or lists of strings, where a list replaces whole on every push andnulldeletes a key - Contact: when an
emailis present, the customer’s contact record is created or updated, and the same tag operation is mirrored onto the contact so email recipient conditions stay in sync
Identify is order-independent. A fresh install usually calls it before the store has appeared in your synced Partner data; the tag and profile operations are then parked (the response reports customerTagsPending / shopProfilePending instead of …Updated) and apply automatically after the next sync makes the store resolvable.
Limits & billing
Every plan includes a monthly allowance of custom events, counted per calendar month (UTC). Only newly stored events count: insertId duplicates and blocked events never do.
| Plan | Included events / month |
|---|---|
| Free | 500 |
| Tier 1-4 | 50,000 |
| Tier 5+ and Enterprise | 250,000 |
- Usage billing (opt-in): enable it on the Billing page and ingest continues past the allowance at $1 per 1,000 events over it, added to your next invoice
- Without the opt-in: once the allowance is used up, new events are rejected (the UI shows “Events paused, limit reached”) until your usage resets at the start of the next month; nothing already stored is lost
- Organization owners get an email at 90% and again at 100% of the allowance, once per month each
- A usage meter on the Custom events pages and a Monthly usage card on the Billing page show month-to-date events, the included allowance and the overage state at any time
Using the data
The Custom events sidebar entry has four tabs:
- Home: your saved reports as live cards, the place your team lands
- Explorer: the chart builder. Up to several metrics per chart, each an event with a measure (count, unique IDs, or sum / average / min / max over a numeric property), bucketed by day, week or month, with property filters, a group-by breakdown and a previous-period comparison. Save any result as a report
- Events: the newest-first raw feed with name and app filters, plus the event catalog where you describe, hide or block event names (blocked names are dropped at ingest)
- Users: every identity that ever sent an event (its
distinctId, or the store domain), with search and latest activity
Custom events also plug into Slack notification rules: a rule row picks an event name and renders a message template with Liquid placeholders ({{ properties.plan }}, {% if %}…) against each incoming event, so the Slack line reads exactly how your team wants it.