Build your first app
Build a dashboard that compares customer spending with your average lifetime value.
Build a customer dashboard that compares one customer's spending with your average customer lifetime value. The script prints a summary like:
jane@example.com — $1840.50 over 12 orders (brand average LTV $512.50)The Quickstart gets you your first GET /v1/customers
response. This guide builds on it with two reads: one customer and
average customer spending.
Prerequisites
- Customer data features enabled and a connected Shopify store. Without them the commerce endpoints return
403(see Commerce data). - A
vyg_API key. It needscustomers:readandinsights:read. Create one as in the Quickstart; available permissions are covered in API keys.
Authenticate with the key as a bearer token. Base URL: https://api.vyg.app/v1.
Store your key in an environment variable so it never lands in source control:
export VYG_API_KEY="vyg_your_key_here"Step 1 — Fetch a customer
GET /v1/customers/{id} returns one
customer as a bare object. The {id} is a customer id; find it with GET /v1/customers/search?query=, which
matches an email or phone prefix.
curl -s "https://api.vyg.app/v1/customers/8f2b0c1e-5d7a-4b7e-9a51-2c6f1e0d3a44" \
-H "Authorization: Bearer $VYG_API_KEY"const API = 'https://api.vyg.app';
const auth = {
headers: { Authorization: `Bearer ${process.env.VYG_API_KEY}` },
};
const CUSTOMER_ID = '8f2b0c1e-5d7a-4b7e-9a51-2c6f1e0d3a44';
const res = await fetch(`${API}/v1/customers/${encodeURIComponent(CUSTOMER_ID)}`, auth);
const customer = await res.json();{
"id": "8f2b0c1e-5d7a-4b7e-9a51-2c6f1e0d3a44",
"created_at": "2024-02-01T10:00:00.000Z",
"email": "jane@example.com",
"phone": "+15551234567",
"currency": "USD",
"orders_count": 12,
"total_spend": 1840.5,
"total_refunded": 0,
"last_order_at": "2026-06-20T14:30:00.000Z",
"coverage_from": "2024-02-01T10:00:00.000Z",
"first_order_at": "2024-02-01T10:00:00.000Z",
"events_30d": 41,
"sessions_30d": 6,
"product_views_30d": 19,
"last_event_at": "2026-06-30T09:12:00.000Z",
"identities": [
{
"type": "email",
"value": "jane@example.com",
"source": "shopify",
"verified": true
}
],
"merged_customer_ids": []
}orders_count,total_spend,total_refundedreflect the order history available in VYG.identitieslists the identifiers resolved for the customer.merged_customer_idslists customers merged into this one.
Step 2 — Get average customer spending
GET /v1/insights/ltv returns the average customer spending
you'll show alongside the customer.
curl -s "https://api.vyg.app/v1/insights/ltv" \
-H "Authorization: Bearer $VYG_API_KEY"Customer spending, grouped into ranges:
{
"scope": "your-shop.myshopify.com",
"total_revenue": "2050.00",
"customer_count": 4,
"average_ltv": "512.50",
"currency_code": "USD",
"distribution": [
{ "label": "0-100", "min": 0, "max": 100, "customer_count": 1 },
{ "label": "100-250", "min": 100, "max": 250, "customer_count": 1 },
{ "label": "250-500", "min": 250, "max": 500, "customer_count": 1 },
{ "label": "500-1000", "min": 500, "max": 1000, "customer_count": 0 },
{ "label": "1000+", "min": 1000, "max": null, "customer_count": 1 }
]
}Brand LTV amounts are decimal strings in the store's currency, and every metric counts completed orders only. See Insights for the full definitions.
Step 3 — Assemble a minimal dashboard
Now wire the calls together. This script is dependency-free — it uses the built-in
fetch (Node 18+ or Bun) and reads the key from VYG_API_KEY. It fetches the
customer and average lifetime value in parallel, then renders both a one-line summary and a
small HTML card.
function formatMoney(amount, currency) {
return new Intl.NumberFormat(
'en-US',
currency ? { style: 'currency', currency } : { minimumFractionDigits: 2, maximumFractionDigits: 2 }
).format(Number(amount));
}
const API = 'https://api.vyg.app';
const KEY = process.env.VYG_API_KEY;
async function get(path) {
const res = await fetch(`${API}${path}`, {
headers: { Authorization: `Bearer ${KEY}` },
});
if (!res.ok) {
const body = await res.json().catch(() => ({}));
throw new Error(`${res.status} ${body.error?.code ?? 'request_failed'}`);
}
return res.json();
}
async function loadDashboard(customerId) {
const id = encodeURIComponent(customerId);
const [customer, brand] = await Promise.all([get(`/v1/customers/${id}`), get('/v1/insights/ltv')]);
const name = customer.email ?? customer.id;
const spend = formatMoney(customer.total_spend, customer.currency);
const brandAverage = formatMoney(brand.average_ltv, brand.currency_code);
const orders = customer.orders_count;
return { name, spend, orders, brandAverage };
}
function summaryLine(d) {
return `${d.name} — ${d.spend} over ${d.orders} orders (brand average LTV ${d.brandAverage})`;
}
function renderCard(d) {
return `<article class="customer-card">
<h2>${d.name}</h2>
<p>Spend: <strong>${d.spend}</strong> over ${d.orders} orders (brand average ${d.brandAverage})</p>
</article>`;
}
loadDashboard('8f2b0c1e-5d7a-4b7e-9a51-2c6f1e0d3a44')
.then((d) => {
console.log(summaryLine(d));
console.log(renderCard(d));
})
.catch((err) => console.error('Dashboard load failed:', err.message));Swap console.log(renderCard(d)) for wherever your app renders — inject the HTML
string into the DOM, return it from a request handler, or map it into your
framework of choice.
Handle errors
Every error is { "error": { "code": "...", "message": "..." } }. Check the HTTP status and
branch on error.code, not on the message text (see Errors):
| Status | error.code | What it means |
|---|---|---|
401 | unauthorized | Missing or invalid key. |
403 | insufficient_scope | Customer data features are unavailable, no Shopify store is connected, or your key needs additional permissions. |
404 | not_found | No customer was found for this ID. |
429 | rate_limited | Too many requests (for example on the products catalog) — wait the Retry-After seconds and retry. |
Next steps
- Customers reference: filter and rank your whole customer base.
- Commerce reference: orders, subscriptions and the live product catalog.
- Insights reference: RFM tiers and customers at risk.
- Commerce data and Insights: the data model and metric definitions behind these endpoints.