Developer Docs
WAVE is Workflow & Analytics for Value-Driven Engagement. It learns from every event, builds behavioral profiles, and runs journeys that engage customers at exactly the right moment.
Architecture — Four Planes
WAVE is organized into four internal planes. Everything you see in the dashboard belongs to exactly one plane. Understanding the planes makes the product navigation predictable.
Receives events from all adapters.
Integrations → Sources · Imports · Webhooks
Builds the Event Catalog, computes Metrics, maintains Classifications, and updates Segments automatically.
Audience → Customers · Segments · Data → Events · Event Catalog · Intelligence → Metrics · Classifications
Evaluates journeys and campaigns, tracks Goals, delivers push, email, WhatsApp, and in-app banners.
Automation → Journeys · Campaigns · Automation Rules · Scheduled Events
Records every decision — who entered, what matched, what was sent, whether the Goal was achieved.
Automation → Executions · Automation Logs · Analytics
Core Concepts
Event
Something that happened in your system. Every event has a name (transfer_completed), a customer ID, and optional properties (amount, currency). Events are immutable once ingested.
Customer
A person in your system. WAVE identifies customers by the customer_id you provide. Attributes (name, tier, city) enrich their profile.
Metric
A behavioral measure computed automatically from event history — engagement_score, days_since_last_event, event counts. Updated on every event, no batch jobs.
Classification
A label derived from Metric thresholds using IF/THEN rules — e.g. if total_spend_30d > 5,000,000 → VIP; otherwise → Regular. Classifications are first-class segment predicates and appear on every customer profile.
Segment
A reusable audience filter built from customer attributes, Metrics, and Classifications. Re-evaluates automatically as new data arrives.
Journey
An automated rule: when a customer triggers a specific event AND meets a condition, take an action. Built in Journey Studio with a visual editor. Journeys are versioned — publishing creates a new version, never replaces an existing one.
Goal
A success condition attached to a journey. Example: "customer places an order within 24 hours of receiving the abandoned-cart push." WAVE tracks whether each execution achieved its Goal, timed out, or is still pending.
Campaign
A one-time manual broadcast to a segment. Unlike journeys, campaigns are triggered by you, not by customer behavior.
Execution
A record of one journey running for one customer. The Executions page (Assurance Plane) shows which version matched, what was delivered, and whether the Goal was achieved.
Quick Start
Five steps from zero to your first automated push notification.
-
1
Get your API key
Log in to WAVE → Settings → General → Developer section → copy the API key. Send it as the
X-Api-Keyheader on every request. -
2
Send your first event
curl -X POST https://wavebitz.com/api/v1/events \ -H "Content-Type: application/json" \ -H "X-Api-Key: YOUR_KEY" \ -d '{ "customer_id": "usr_001", "event_name": "app_opened", "properties": { "screen": "home" } }' -
3
Check the Event Catalog
WAVE automatically discovers event types. Open Data → Event Catalog — you'll see
app_openedappear with its properties. No configuration needed. -
4
Register a push device
From your mobile app, call
POST /api/v1/devices/registerwith the FCM token. This links the device to the customer so WAVE can deliver push notifications. -
5
Create a Journey
Go to Automation → Journeys → New Journey. Set the trigger to
app_opened, add a push action, and click Publish.
Analytics
Analytics → Analytics
The Analytics page is the health dashboard for your Execution Plane. It shows campaign-level delivery stats and journey execution counts side by side.
Campaigns
Every campaign with its channel (push / email), target segment, delivery count, and send timestamp.
Journeys
Each published journey version with its status and total execution count since publication.
Customers
Audience → Customers
WAVE creates a customer record the first time it sees a customer_id. You can enrich that record at any time using the Identify endpoint, which merges attributes into the existing profile.
Customer Profile
The customer profile has three layers:
- Attributes — arbitrary key/value pairs you send (name, tier, city, birth_date)
- Metrics — behavioral measures computed from event history
- Classifications — labels derived from metric thresholds you define
Customer Timeline
The customer detail page shows the complete activity timeline for a customer — four orthogonal layers in reverse chronological order. Use it to debug a journey or verify that events are arriving correctly.
- Event — raw behavioral events from your app (e.g.
cart_abandon_detected) - Journey Enrolled / Rejected — whether the customer entered or was blocked from a journey, with the reason
- Action Executed — Wave fired an automation rule (send email, send push, etc.)
- Delivery — provider-level confirmation; shows Opened and Clicked timestamps inline when available
{
"name": "Budi Santoso",
"tier": "premium",
"city": "Jakarta",
"birth_date": "1990-04-15",
"balance": 12500000
}
Events
Data → Events
The Events is the raw, append-only record of every event WAVE has received. Search and filter by customer ID, event name, or time range. Events are immutable — they cannot be edited or deleted after ingestion.
Event Catalog
Data → Event Catalog
The Event Catalog is WAVE's schema layer. It discovers event types automatically as events arrive — you never define schemas upfront. Every unique event name becomes a catalog entry, along with all property keys WAVE has seen for that type.
The Catalog powers Journey Studio — when you pick a trigger event, the condition editor knows which properties are available and their observed types.
Metrics
Intelligence → Metrics
Metrics are behavioral measures computed automatically from a customer's event history. They update on every event — no batch jobs, no manual refresh. Metrics appear on the customer profile and can be used as segment predicates and journey conditions.
Built-in Metrics
| Metric | Description |
|---|---|
engagement_score | 0–100 score based on recency, frequency, and diversity of events |
days_since_last_event | Days since the customer's most recent event |
total_events | Total number of events ingested for this customer |
events_last_30d | Event count in the last 30 days |
days_since_last_event is the primary signal for the customer_dormant_detected Scheduled Event. Set the dormancy threshold under Automation → Scheduled Events.
Classifications
Intelligence → Classifications
Classifications let you attach a meaningful label to every customer using IF/THEN rules over your Metrics. Each rule is evaluated top-to-bottom — the first match wins, and the result is stored on the customer profile and updated whenever a relevant Metric changes.
Example Classifications
| Name | Rules (first match wins) |
|---|---|
customer_tier | total_spend_30d > 5,000,000 → VIP · total_spend_30d > 1,000,000 → Regular · otherwise → New |
activity_status | days_since_last_event < 7 → Active · days_since_last_event < 30 → Passive · otherwise → Dormant |
risk_level | failed_login_count_7d > 5 → High · failed_login_count_7d > 2 → Medium · otherwise → Low |
Classifications appear on every customer profile and can be used in Segment rules and journey entry conditions alongside Metrics and attributes.
classification.changed event. Use it as a Journey trigger to react to tier transitions in real time. Properties: field (Classification slug), previous (old label or null), current (new label).
Segments
Audience → Segments
Segments are reusable audience filters. You define rules using customer attributes, Metrics, and Classifications — and WAVE evaluates membership automatically as new data arrives.
Creating a Segment
- Go to Audience → Segments → New Segment
- Name the segment (e.g. High-Value Active Customers)
- Add conditions:
total_spend_30d > 1000000ANDdays_since_last_event < 14 - Save. WAVE immediately evaluates membership across all customers.
Predicate types
- Attribute predicates — filter on customer profile fields (tier, city, birth_date)
- Metric predicates — filter on engagement_score, days_since_last_event, events_last_30d
- Classification predicates — filter on any Classification you've defined
- Event predicates — filter on event history (has / has not performed an event)
Using Segments
- Journey entry filter — only customers in the segment can enter the journey
- Campaign target — broadcast to everyone in the segment at once
Journeys
Automation → Journeys
Journey Studio is the visual builder for customer journeys. Journeys evaluate in real-time — there is no batch delay. Every journey is versioned: publishing creates a new version, never replaces an existing one.
Anatomy of a Journey
Start when
The event that starts evaluation. Select from your Event Catalog. Add property filters to narrow down — e.g. only fire when amount > 10000000.
Who should enter? (optional)
Entry conditions checked against the customer profile at trigger time — Metrics, Classifications, attributes, or event history. Customers who don't match are blocked from entering.
Wait (optional)
Wait before taking action. Example: send the notification 30 minutes after the trigger. The execution is queued and fires automatically.
Message
What WAVE sends: push notification or email. Channel options — Push, Email, or Smart Delivery (push if device token exists, email otherwise). The message body can reference event properties using {{ event.amount }}.
Goal (optional)
A success condition with a time window. WAVE tracks whether each execution achieved the Goal, timed out, or is still pending.
Goal States
| State | Meaning |
|---|---|
| Achieved | The customer performed the goal event within the time window |
| Pending | The time window is still open |
| Timed out | The window expired without the customer completing the goal |
| None | This journey has no goal defined |
Campaigns
Automation → Campaigns
Campaigns are one-time broadcasts to a segment. Unlike journeys, they are triggered manually and are not repeated.
Creating a Campaign
- Go to Automation → Campaigns → New Campaign
- Name the campaign and choose channel: Push or Email
- Choose target: All Customers or a specific Segment
- Compose the message
- Click Launch — WAVE delivers to all qualifying customers immediately
Automation Rules
Automation → Automation Rules
Automation Rules are the lower-level rule engine that predates Journey Studio. They support more complex trigger types including dormancy windows, inactivity guards, and event sequence detection (did X, never did Y).
| Trigger type | Description |
|---|---|
| Event-based | Fires when a named event arrives, with an optional property condition |
| Dormancy | Fires when a customer has not been seen for N days |
| Emit Scheduled Event | Converts the rule match into a synthetic event (e.g. customer_dormant_detected) |
| Event Sequence | Fires when a customer did event A but never did event B within a time window |
All rule executions appear in Automation → Automation Logs and in the Executions page.
Scheduled Events
Automation → Scheduled Events
Scheduled Events are synthetic events emitted by WAVE on a timer — no client code required. They enter the Events and trigger journeys just like any other event.
| Event name | When it fires |
|---|---|
customer_dormant_detected |
When a customer's last event was more than N days ago (N = configurable) |
customer_birthday_today |
For customers whose birth_date attribute matches today's date |
scheduler.
Executions
Automation → Executions
The Executions page is WAVE's Assurance Plane. For every journey execution, it records:
- Which customer triggered it and which event
- The event properties at the time of evaluation
- Which journey version was active
- Whether the condition passed or failed, and why
- What action was taken and when it was delivered
- Whether the Goal was achieved, pending, or timed out
Sources
Integrations → Sources
A Source is how behavior events enter WAVE. Every event carries a source type that records where it came from. The Sources page shows live event volume and health per source type.
| Source | Type | Best for |
|---|---|---|
| REST API | api | Backend services sending events in real-time |
| Flutter SDK | sdk | Mobile apps (Android, iOS) |
| JavaScript SDK | sdk | Web apps and browser-based frontends |
| CSV Event Imports | csv | Historical data migration or batch event uploads |
| Webhooks | webhook | Legacy or config-only systems that can only be given a URL — use the REST API instead if your backend can make HTTP calls |
| Scheduled Events | scheduler | Time-based synthetic events: dormancy detection, birthdays, inactivity |
Imports
Integrations → Imports
The Imports page is a unified audit trail for all batch data entering WAVE — both customer profile imports and event CSV imports.
| Type | How to upload | What it does |
|---|---|---|
| Customer CSV | Integrations → Imports → Upload Customer CSV | Creates or updates customer profiles (name, email, attributes) |
| Event CSV | Integrations → Imports → Upload Event CSV | Ingests historical events into the Events |
Every import — success or failure — is logged with: Time · Type · Source file · Rows · Success · Failed · Duration.
Download the CSV template from the Imports page to see the expected column format before uploading.
Webhooks
Integrations → Webhooks
The Webhooks page lets you create inbound webhook endpoints. Each endpoint has a unique URL and an HMAC signing secret. External systems POST events to the URL; WAVE verifies the signature before ingesting.
- Go to Integrations → Webhooks → Add Endpoint
- Give it a name (e.g. "Core Banking") — this becomes the
source_slug - Save. Copy the signing secret that appears — it is shown only once.
- Send events to the URL using HMAC-SHA256 signature verification
POST /api/v1/webhooks/{tenantSlug}/{sourceSlug}
See the Webhook API section in the Developer Guide for signature generation code in Python, Node.js, and PHP.
Use the Events API if your backend can make HTTP calls — it is simpler (just an API key header, no signature). Use a webhook endpoint if you are integrating a legacy or config-only system that only accepts a URL and cannot add custom request headers.
Settings
Settings → Settings
Timezone
Set your display timezone. All timestamps in the dashboard (events, executions, imports) are shown in this timezone. Events are always stored in UTC internally.
API Key
Your tenant API key (wv_...). Include this as X-Api-Key on every API request. Keep it secret — it has full write access to your event stream.
Email / SMTP
Configure your outgoing SMTP server to enable email campaigns and journey email actions.
| SMTP Host | e.g. smtp.gmail.com |
| Port | 587 (TLS) or 465 (SSL) |
| Username | Your SMTP login (usually your email address) |
| Password | For Gmail: use an App Password, not your Google password |
| From Address | The sender address recipients will see |
| From Name | The sender display name (e.g. "Nusantara Bank") |
Push Notifications / FCM
By default, WAVE uses its own Firebase project for push delivery. To use your own Firebase project (so notifications appear from your app's sender ID), upload your Firebase service account JSON.
- Go to Firebase Console → your project → Project Settings → Service Accounts
- Click Generate new private key → download the JSON file
- Open the file, copy its full contents, paste into Settings → Push Notifications → Service Account JSON
- Save. Your project ID will appear confirming the credentials are active.
Downloads
Everything you need to integrate WAVE into your application.
Flutter SDK
wave-flutter-sdk.zip — Dart package for iOS & Android
Download .zip
React Native SDK
wave-react-native-sdk.zip — TypeScript package, Expo-compatible
Download .zip
Postman Collection
Wave.postman_collection.json — all API endpoints ready to run
Download .json
JavaScript SDK
wave.js — browser SDK for web apps, no build step required
Download .js
Authentication
WAVE uses two authentication mechanisms depending on the endpoint group.
Platform API (Backend → WAVE)
All platform endpoints require your tenant API key:
X-Api-Key: wv_xxxxxxxxxxxxxxxxxxxx
Customer API (Mobile App → WAVE)
Customer-facing endpoints accept either:
- A Bearer token issued by
POST /api/v1/auth - An X-Customer-Id header (for trusted server-side calls)
# Bearer token
Authorization: Bearer <token>
X-Api-Key: wv_xxxx
# Direct customer ID (server-side only)
X-Customer-Id: usr_001
X-Api-Key: wv_xxxx
Track Events
POST /api/v1/events
The primary ingestion endpoint. Call this from your backend whenever a significant business event occurs.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
customer_id | string | Yes | Your unique identifier for this customer |
event_name | string | Yes | Event name in snake_case (e.g. transfer_completed) |
properties | object | No | Arbitrary key/value pairs describing the event |
occurred_at | ISO 8601 | No | Business time of the event. Defaults to now. |
idempotency_key | string | No | Unique key to prevent duplicate ingestion on retry |
occurred_at in UTC (2026-07-04T03:00:00Z) or with an explicit offset (2026-07-04T10:00:00+07:00). Timestamps without timezone information are treated as UTC.
Example
curl -X POST https://wavebitz.com/api/v1/events \
-H "Content-Type: application/json" \
-H "X-Api-Key: wv_xxxx" \
-d '{
"customer_id": "usr_001",
"event_name": "transfer_completed",
"occurred_at": "2026-07-04T03:00:00Z",
"idempotency_key": "trx-7f3a9c2d",
"properties": {
"amount": 2500000,
"destination_bank": "BCA",
"currency": "IDR"
}
}'
Response
// Success
{ "success": true, "event_id": "018f3a9c-..." }
// Deduplicated (idempotency_key seen before)
{ "success": true, "deduplicated": true }
Batch Events
POST /api/v1/events/batch
Send up to 1,000 events in a single HTTP request. The batch is durably accepted before any processing begins — WAVE returns 202 Accepted once the batch is committed to storage, then processes events asynchronously. Useful for high-throughput backends, mobile SDKs that buffer offline events, or historical data migrations via an Import Session.
Request
Optional batch-level header:
Idempotency-Key: <unique-string> # optional — deduplicates the whole batch on retry
| Field | Type | Required | Description |
|---|---|---|---|
events | array | Yes | Array of event objects. Min 1, max 1,000 per request. |
events[].customer_id | string | Yes | Your unique identifier for this customer |
events[].event_name | string | Yes | Event name in snake_case |
events[].properties | object | No | Arbitrary key/value pairs |
events[].occurred_at | ISO 8601 | No | Defaults to ingestion time if omitted |
events[].idempotency_key | string | No | Per-event deduplication key |
import_id | UUID | No | Links this batch to an Import Session. Required for historical mode. |
Example
curl -X POST https://wavebitz.com/api/v1/events/batch \
-H "Content-Type: application/json" \
-H "X-Api-Key: wv_xxxx" \
-H "Idempotency-Key: batch-2026-07-04-001" \
-d '{
"events": [
{
"customer_id": "usr_001",
"event_name": "login",
"occurred_at": "2026-07-04T03:00:00Z",
"idempotency_key": "login-usr001-0704"
},
{
"customer_id": "usr_001",
"event_name": "transfer_completed",
"occurred_at": "2026-07-04T03:02:00Z",
"idempotency_key": "trx-7f3a9c2d",
"properties": {
"amount": 2500000,
"currency": "IDR"
}
},
{
"customer_id": "usr_002",
"event_name": "product_viewed",
"properties": { "product_id": "P-9901" }
}
]
}'
Response — 202 Accepted
{
"batch_id": "018f3a9c-...",
"event_count": 3,
"status": "accepted"
}
The batch is durably stored at this point. Processing happens asynchronously — use an Import Session to track per-batch progress for large migrations.
batch_id is returned immediately — no new batch is created and admission control is not checked. Safe to retry on network failures.
503 with a Retry-After header (seconds). Implement exponential back-off and respect this header. Idempotent retries of already-accepted batches always return 202 regardless of queue depth.
Import Sessions
An Import Session groups multiple batches under a single tracked operation with an explicit lifecycle. Required for historical data migrations — live ingestion does not need one.
Create a Session
POST /api/v1/imports
| Field | Type | Required | Description |
|---|---|---|---|
mode | string | No | live (default) or historical. Use historical for backfills. |
automation_policy | string | No | trigger (default for live) or suppress. Historical defaults to suppress. |
ci_policy | string | No | recompute (default) or defer. Defer CI until after the full import completes. |
label | string | No | Human-readable name for this import (e.g. "2024 transaction history") |
Policies are immutable — they cannot be changed after the session is created.
| Mode | automation_policy | ci_policy | Behaviour |
|---|---|---|---|
| live | trigger | recompute | Normal real-time behavior — journeys fire, CI updates per event |
| historical | suppress | recompute | Journeys suppressed, CI updated per event (slower for large imports) |
| historical | suppress | defer | Journeys suppressed, CI deferred until manual reconciliation — fastest for large backfills |
// Create a historical import session (journeys suppressed, CI deferred)
curl -X POST https://wavebitz.com/api/v1/imports \
-H "Content-Type: application/json" \
-H "X-Api-Key: wv_xxxx" \
-d '{
"mode": "historical",
"ci_policy": "defer",
"label": "2024 transaction history"
}'
// Response — 201 Created
{
"id": "018f3a9c-...",
"mode": "historical",
"automation_policy": "suppress",
"ci_policy": "defer",
"status": "open",
"ci_status": "deferred",
"ci_reconciled_at": null,
"created_at": "2026-10-19T08:00:00Z"
}
Track Progress
GET /api/v1/imports/{id}
{
"id": "018f3a9c-...",
"status": "open",
"ci_status": "deferred",
"ci_reconciled_at": null,
"progress": {
"batches": { "total": 120, "processed": 118, "failed": 0, "pending": 2 },
"events": { "total": 119840, "processed": 117600, "failed": 0, "pending": 2240 }
}
}
| ci_status | Meaning |
|---|---|
current | CI is up to date (live mode or historical+recompute) |
deferred | Import still in progress; CI will be reconciled after completion |
pending_reconciliation | Import completed; CI reconciliation not yet run |
reconciled | CI fully up to date; ci_reconciled_at is set |
Complete a Session
POST /api/v1/imports/{id}/complete
Closes the session. After this call, new batches against this import_id are rejected with 422 import_closed. Idempotent — safe to call more than once. Returns the current progress + ci_status: "pending_reconciliation" for deferred sessions.
batches.pending == 0 and the session is completed, run the Artisan command to bring customer intelligence up to date:
php artisan wave:reconcile-ci-import {importId}
This dispatches RecomputeCustomerIntelligence for every affected customer. It is idempotent and reads from the canonical event ledger — it does not re-trigger automations.
Full Migration Flow
// 1. Create session
POST /api/v1/imports → { "id": "abc-123", ... }
// 2. Stream batches (repeat until all events are submitted)
POST /api/v1/events/batch { "import_id": "abc-123", "events": [...] }
// 3. Close the session
POST /api/v1/imports/abc-123/complete
// 4. Poll until all batches are processed
GET /api/v1/imports/abc-123 → progress.batches.pending == 0
// 5. Reconcile CI (deferred imports only)
php artisan wave:reconcile-ci-import abc-123
// 6. Confirm
GET /api/v1/imports/abc-123 → ci_status: "reconciled"
Identify Customers
POST /api/v1/customers/identify
Creates or updates a customer profile. Safe to call repeatedly — attributes are merged, not replaced.
{
"customer_id": "usr_001",
"attributes": {
"name": "Budi Santoso",
"tier": "premium",
"city": "Jakarta",
"birth_date": "1990-04-15"
}
}
Batch Identify
POST /api/v1/customers/identify/batch
Upsert up to 1,000 customer profiles in a single request. Same merge semantics as the single identify call — existing attributes not in the payload are preserved. Useful for bulk imports, nightly sync jobs, or any server-side operation where you need to upsert many customers at once.
{
"customers": [
{
"customer_id": "usr_001",
"attributes": {
"name": "Budi Santoso",
"email": "budi@example.com",
"tier": "premium"
}
},
{
"customer_id": "usr_002",
"attributes": {
"name": "Siti Rahayu",
"email": "siti@example.com",
"tier": "standard"
}
}
]
}
// Response
{
"count": 2,
"results": [
{ "customer_id": "usr_001", "success": true },
{ "customer_id": "usr_002", "success": true }
]
}
Each batch request counts as one request against the identify rate limit (default 60 req/min). Results are returned in the same order as the input array.
Location
Attach a location object to any
identify call or to any event's
properties — Wave extracts and indexes it automatically,
enabling within area and within radius segment predicates.
The most recent location seen across all calls wins.
Via identify
{
"customer_id": "usr_001",
"attributes": {
"location": {
"lat": -6.2088,
"lng": 106.8456,
"city": "Jakarta",
"district": "Gambir",
"postal_code": "10110",
"source": "gps" // "gps" | "ip" | "manual"
}
}
}
Via any event
{
"customer_id": "usr_001",
"event_name": "check_in",
"properties": {
"location": {
"lat": -6.2088,
"lng": 106.8456,
"city": "Jakarta",
"source": "gps"
},
"venue": "Grand Indonesia"
}
}
Tips:
Omit lat and lng if coordinates are unavailable — do not pass 0, 0 as a placeholder.
The text fields (city, district, postal_code)
are optional and power the within area predicate independently of coordinates.
Register Devices
POST /api/v1/devices/register
Links an FCM push token to a customer. Call this on app launch after the user is authenticated, and whenever the FCM token refreshes.
{
"customer_id": "usr_001",
"token": "fcm-token-here",
"platform": "android" // "android" | "ios"
}
Customer API
Endpoints for customer-facing apps. All require X-Api-Key plus a Bearer token or X-Customer-Id.
POST /api/v1/auth
Authenticate a customer and receive a bearer token (valid 8 hours).
// Request
{ "customer_id": "usr_001", "pin": "123456" }
// Response
{ "token": "abc123...", "customer": { "id": "usr_001", "name": "Budi", "tier": "premium" } }
GET /api/v1/me
Returns the authenticated customer's profile and attributes.
GET /api/v1/me/executions
Lists the 20 most recent journey executions for the customer.
GET /api/v1/me/notifications
Lists the 30 most recent push notifications delivered to the customer.
Content Cards
Content Cards are persistent in-app messages that live in a feed inside your app — unlike push notifications, they remain visible until the customer dismisses them. Wave delivers them through journeys and campaigns; your app reads and manages them via these endpoints.
GET /api/v1/content-cards
Returns all active (non-dismissed) content cards for the authenticated customer. Poll this on app launch and after completing a key action.
// Response
[
{
"id": "cc_01j8p...",
"title": "You have Rp 50,000 cashback waiting",
"body": "Complete a transfer before Sunday to claim it.",
"image_url": null,
"cta_label": "Transfer Now",
"cta_url": "/transfer",
"created_at": "2026-07-04T03:00:00Z"
}
]
POST /api/v1/content-cards/{id}/impression
Record that the customer saw this card. Call when the card becomes visible in the viewport. Safe to call once per session — subsequent calls for the same card are ignored.
POST /api/v1/content-cards/{id}/click
Record that the customer tapped the card's CTA. WAVE logs this as an engagement event on the customer timeline and on the originating journey execution.
POST /api/v1/content-cards/{id}/dismiss
Mark the card as dismissed. It will no longer appear in the GET /content-cards response for this customer.
CSV Import
Upload historical or batch data via CSV. Go to Integrations → Imports and use the upload button for events or customer profiles. Download the template from the same page to see the exact column format.
Event CSV — Required Columns
| Column | Required | Description |
|---|---|---|
customer_id | Yes | Your unique customer identifier |
event_name | Yes | Event name in snake_case |
occurred_at | No | ISO 8601 timestamp. Defaults to import time. |
[any other column] | No | Additional columns become event properties automatically |
Example
customer_id,event_name,occurred_at,amount,currency
usr_001,transfer_completed,2026-01-15T01:30:00Z,2500000,IDR
usr_002,top_up,2026-01-15T02:00:00Z,500000,IDR
Webhook API
Webhooks let external systems push events to WAVE without using the platform API key. Each webhook endpoint is created under Integrations → Webhooks and has its own signing secret.
Endpoint URL
POST /api/v1/webhooks/{tenantSlug}/{sourceSlug}
Payload Format
The webhook payload uses a flat JSON structure — no nested properties object. Fields customer_id and event_name are required; all other fields become event properties automatically.
{
"customer_id": "usr_001",
"event_name": "purchase_completed",
"occurred_at": "2026-07-04T09:00:00+07:00",
"idempotency_key": "ord-b001-purchase",
"order_id": "ORD-B001",
"amount": 2000000,
"product_name": "Flexi Loan"
}
HMAC Signature
Compute HMAC-SHA256 over the raw request body bytes using your endpoint secret. Send it in the X-Wave-Signature header.
// Python
import hmac, hashlib, json
body = json.dumps(payload, separators=(",", ":")).encode()
sig = "sha256=" + hmac.new(SECRET.encode(), body, hashlib.sha256).hexdigest()
# Header: X-Wave-Signature: {sig}
// PHP
$body = json_encode($payload, JSON_UNESCAPED_UNICODE);
$sig = 'sha256=' . hash_hmac('sha256', $body, $secret);
// Node.js
const body = JSON.stringify(payload);
const sig = 'sha256=' + crypto.createHmac('sha256', SECRET).update(body).digest('hex');
Read API
The Read API exposes customer intelligence, segment membership, campaign delivery status, and journey execution history. All endpoints are scoped to the authenticated tenant and require an X-Api-Key header. Rate limit: 120 requests/min per tenant.
| Endpoint | Returns |
|---|---|
| GET /api/v1/customers/{id}/intelligence/fields | All Calculated Fields |
| GET /api/v1/customers/{id}/intelligence/fields/{slug} | Single Calculated Field |
| GET /api/v1/customers/{id}/intelligence/metrics | All CI Metrics |
| GET /api/v1/customers/{id}/intelligence/metrics/{slug} | Single CI Metric |
| GET /api/v1/customers/{id}/segments | Segment membership |
| GET /api/v1/customers/{id}/campaigns/{cid}/delivery | Campaign delivery status |
| GET /api/v1/customers/{id}/executions | Journey execution history |
| POST /api/v1/customers/identify/batch | Upsert up to 100 customer profiles |
| GET /api/v1/journeys/{id}/deliveries | All deliveries for a journey (paginated) |
Calculated Fields
Returns all tenant-defined Calculated Fields for the customer. Fields that have not been computed yet are included with status: "no_data".
GET /api/v1/customers/{customerId}/intelligence/fields GET /api/v1/customers/{customerId}/intelligence/fields/{slug}
// GET /api/v1/customers/usr_001/intelligence/fields
{
"customer_id": "usr_001",
"data": [
{
"field": "loyalty_tier",
"label": "Loyalty Tier",
"value": "gold",
"status": "available",
"computed_at": "2026-10-20T05:00:00.000000Z"
},
{
"field": "risk_segment",
"label": "Risk Segment",
"value": null,
"status": "no_data",
"computed_at": null
}
]
}
CI Metrics
Returns all tenant-defined CI Metrics with computed float values. The -1.0 sentinel (stored when no matching events exist) is normalized to null with status: "no_data" — it never reaches the API response.
GET /api/v1/customers/{customerId}/intelligence/metrics GET /api/v1/customers/{customerId}/intelligence/metrics/{slug}
// GET /api/v1/customers/usr_001/intelligence/metrics
{
"customer_id": "usr_001",
"data": [
{
"metric": "total_orders",
"label": "Total Orders",
"value": 42.0,
"status": "available",
"computed_at": "2026-10-20T05:00:00.000000Z"
},
{
"metric": "days_since_last_purchase",
"label": "Days Since Last Purchase",
"value": null,
"status": "no_data",
"computed_at": null
}
]
}
Segment Membership
Returns the segments the customer currently belongs to — evaluated live at request time. An empty array means the customer matches no segments (not an error).
GET /api/v1/customers/{customerId}/segments
// GET /api/v1/customers/usr_001/segments
{
"customer_id": "usr_001",
"data": [
{ "id": 4, "slug": "high-value", "name": "High-Value Customers" },
{ "id": 7, "slug": "reactivation", "name": "Reactivation Candidates" }
]
}
Campaign Delivery
Returns the latest delivery attempt per channel for a specific customer + campaign. Only is_latest = true rows are returned — retried deliveries are coalesced. An empty array means the campaign has not been delivered to this customer yet.
GET /api/v1/customers/{customerId}/campaigns/{campaignId}/delivery
// GET /api/v1/customers/usr_001/campaigns/12/delivery
{
"customer_id": "usr_001",
"campaign_id": 12,
"data": [
{
"channel": "push",
"status": "delivered",
"sent_at": "2026-10-20T05:00:00.000000Z",
"delivered_at": "2026-10-20T05:00:04.000000Z",
"first_opened_at": "2026-10-20T05:03:11.000000Z",
"failure_reason": null
},
{
"channel": "email",
"status": "failed",
"sent_at": "2026-10-20T05:00:00.000000Z",
"delivered_at": null,
"first_opened_at": null,
"failure_reason": "550 Mailbox does not exist"
}
]
}
| status | Meaning |
|---|---|
| queued | Accepted, waiting for a worker |
| sent | Handed off to the provider; receipt not yet confirmed |
| delivered | Provider confirmed delivery (push / WhatsApp) |
| opened | Customer opened (email tracking pixel) |
| failed | Delivery failed; see failure_reason |
| skipped | Suppressed (no token, opted-out, etc.) |
Journey Executions
Returns the customer's journey execution history, ordered most-recent first. Use ?limit= to control page size (default 50, max 100).
GET /api/v1/customers/{customerId}/executions[?limit=50]
// GET /api/v1/customers/usr_001/executions?limit=5
{
"customer_id": "usr_001",
"data": [
{
"id": 1041,
"journey_id": 3,
"journey_name": "Reactivation — 30 days",
"status": "sent",
"fire_at": "2026-10-20T05:00:00.000000Z",
"sent_at": "2026-10-20T05:00:01.000000Z",
"skip_reason": null
},
{
"id": 987,
"journey_id": 1,
"journey_name": "Welcome Series",
"status": "skipped",
"fire_at": "2026-09-01T08:00:00.000000Z",
"sent_at": null,
"skip_reason": "goal_already_met"
}
]
}
| status | Meaning |
|---|---|
| pending | Queued, not yet fired |
| waiting | Paused at a Wait-for-Event step |
| firing | Currently being processed |
| sent | Delivered to the channel |
| failed | Processing failed |
| exited | Left the journey (goal met or branch exit) |
| skipped | Suppressed; see skip_reason |
Journey Deliveries
Returns all delivery records for a journey across every customer who entered it, ordered sent_at DESC. Use this to build custom delivery reports or populate your own analytics. Supports cursor-based pagination via ?cursor=.
GET /api/v1/journeys/{journeyId}/deliveries[?limit=50&cursor=988]
{
"journey_id": 3,
"data": [
{
"customer_id": "usr_001",
"channel": "push",
"status": "opened",
"sent_at": "2026-10-20T05:00:01.000000Z",
"delivered_at": "2026-10-20T05:00:03.000000Z",
"first_opened_at": "2026-10-20T05:04:12.000000Z",
"first_clicked_at": null,
"failure_reason": null
}
],
"meta": {
"limit": 50,
"has_more": true,
"next_cursor": 988
}
}
Pass next_cursor as ?cursor= on the next request to fetch the following page. has_more: false means you have reached the last page. Limit: 1–200, default 50.
Error responses
| HTTP | Cause |
|---|---|
| 401 | Missing or invalid X-Api-Key |
| 404 | Customer or campaign not found in this tenant |
| 422 | Invalid identifier (empty customer_id, campaign_id ≤ 0, etc.) |
| 429 | Rate limit exceeded (120 req/min per tenant) |
Mobile Push Setup
Wave delivers push notifications through your own Firebase project — it never shares a sending identity across tenants. The setup path depends on whether your app already uses Firebase Cloud Messaging.
No FCM yet
Starting fresh — create a Firebase project, add the SDK to your app, and wire it to Wave.
Already using FCM
Firebase is already in your app — add Wave in three places without touching your existing handlers.
Path A — No FCM yet
1. Create a Firebase project
- Go to console.firebase.google.com → Create project
- In Project Settings → Cloud Messaging confirm Cloud Messaging is enabled
- Add your platform app (Android / iOS / Web) via Add app and follow the native setup wizard — this gives you
google-services.json(Android),GoogleService-Info.plist(iOS), or afirebaseConfigobject (Web)
2. Upload your service account to Wave
- Firebase Console → Project Settings → Service accounts → Generate new private key → download the JSON
- Wave dashboard → Settings → Channels → Push Notifications → upload that JSON file
3. Add firebase_messaging to your app and wire the token
Add the Firebase Messaging package for your platform, then pass the token to Wave after identify(). Always listen for token rotation — a stale token causes silent delivery failures.
// pubspec.yaml: add firebase_core, firebase_messaging
await Firebase.initializeApp();
Wave.init(baseUrl: 'https://wavebitz.com', apiKey: 'wv_...');
await Wave.instance.identify(user.id);
final token = await FirebaseMessaging.instance.getToken();
if (token != null) await Wave.instance.registerDevice(token);
FirebaseMessaging.instance.onTokenRefresh.listen((t) => Wave.instance.registerDevice(t));
// npm install @react-native-firebase/app @react-native-firebase/messaging
Wave.init({ baseUrl: 'https://wavebitz.com', apiKey: 'wv_...' });
await Wave.instance.identify(user.id);
const token = await messaging().getToken();
await Wave.instance.registerDevice(token);
messaging().onTokenRefresh(t => Wave.instance.registerDevice(t));
Path B — Already using FCM
Your Firebase project and native SDK are already in place. You need to do three things: upload credentials to Wave, register the token, and guard your existing handlers.
1. Upload your service account to Wave
- Firebase Console → Project Settings → Service accounts → Generate new private key → download the JSON
- Wave dashboard → Settings → Channels → Push Notifications → upload that JSON file
2. Register the token with Wave
Find where you already call getToken() and onTokenRefresh — add the Wave calls there. No new FCM setup needed.
await Wave.instance.identify(user.id);
// Already have this — add the Wave call
final token = await FirebaseMessaging.instance.getToken();
if (token != null) await Wave.instance.registerDevice(token); // ← add
// Already have this — add the Wave call
FirebaseMessaging.instance.onTokenRefresh.listen((t) => Wave.instance.registerDevice(t)); // ← add
await Wave.instance.identify(user.id);
const token = await messaging().getToken(); // already have this
await Wave.instance.registerDevice(token); // ← add
messaging().onTokenRefresh(t => Wave.instance.registerDevice(t)); // ← add
3. Guard your existing handlers
Add one check at the top of each FCM handler. Without it your foreground handler may double-display Wave notifications and your tap handler may misroute them.
FirebaseMessaging.onMessage.listen((message) {
if (Wave.isWaveNotification(message.data)) return; // ← add
showLocalNotification(message);
});
FirebaseMessaging.onMessageOpenedApp.listen((message) {
final id = Wave.deliveryId(message.data);
if (id != null) { Wave.instance.trackPushOpened(id); return; } // ← add
navigateTo(message.data['screen']);
});
messaging().onMessage(async message => {
if (Wave.isWaveNotification(message.data)) return; // ← add
showLocalNotification(message);
});
messaging().onNotificationOpenedApp(async message => {
const id = Wave.deliveryId(message.data);
if (id) { await Wave.instance.trackPushOpened(id); return; } // ← add
navigateTo(message.data.screen);
});
Flutter SDK
The WAVE Flutter SDK wraps the REST API for mobile apps — event tracking, customer identification, push device registration, and more. Timestamps are automatically converted to UTC before sending.
Download wave-flutter-sdk.zipStep 1 — Add the package
Extract the zip into your project, then add it as a local path dependency in pubspec.yaml:
dependencies:
flutter:
sdk: flutter
flutter_wave_sdk:
path: ./flutter_wave_sdk # folder extracted from the zip
Then run flutter pub get.
Step 2 — Initialise at app startup
Call Wave.init() once in main.dart before runApp(). Your API key is in the WAVE dashboard under Settings → Developer.
import 'package:flutter_wave_sdk/wave_flutter.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
Wave.init(
baseUrl: 'https://wavebitz.com',
apiKey: 'wv_your_api_key_here',
);
runApp(const MyApp());
}
Step 3 — Identify the customer on login
Call identify() as soon as you know who the user is — typically after a successful login or session restore. This sets the customer ID for all subsequent track() calls.
await Wave.instance.identify(
'usr_001', // your internal user ID
attributes: {
'name': 'Budi Santoso',
'email': 'budi@example.com',
'tier': 'gold',
},
);
Step 4 — Register the push token and handle token refresh
After calling identify(), register the current FCM token with Wave. FCM rotates tokens periodically — listen for onTokenRefresh and re-register whenever it changes. Skipping this causes silent delivery failures after token rotation.
// Register current token
final fcmToken = await FirebaseMessaging.instance.getToken();
if (fcmToken != null) {
await Wave.instance.registerDevice(fcmToken);
}
// Re-register whenever FCM rotates the token
FirebaseMessaging.instance.onTokenRefresh.listen((newToken) {
Wave.instance.registerDevice(newToken);
});
registerDevice() requires identify() to have been called first, or pass customerId explicitly as the second argument.
Logout and account switching
When a customer logs out, call identify() with a new customer ID on the next login — Wave will associate the device token with the new customer. If you want to stop push delivery immediately on logout (e.g. shared devices), deregister the token first:
// On logout — optional, only needed for shared/public devices
await Wave.instance.deregisterDevice();
// On next login — Wave associates the token with the new customer
await Wave.instance.identify(newUser.id, attributes: { ... });
final token = await FirebaseMessaging.instance.getToken();
if (token != null) await Wave.instance.registerDevice(token);
Step 5 — Report push notification opens
Wave embeds a wave_delivery_id in every push notification's data payload. Call trackPushOpened() when the user taps the notification so WAVE can record the engagement on the Delivery ledger.
// In main() — after Firebase.initializeApp()
FirebaseMessaging.onMessageOpenedApp.listen((message) {
// Handle deep-link navigation
pushNavSignal.value = screenToTab(message.data['screen']);
// Report the open to Wave
final deliveryId = message.data['wave_delivery_id'] as String?;
if (deliveryId != null) {
try {
Wave.instance.trackPushOpened(deliveryId);
} catch (_) {
// Wave not yet initialized — user was logged out when notification arrived.
}
}
});
track() if you also want the tap as a behavioral event.
Step 6 — Track events
Call track() wherever meaningful actions happen. All extra fields go into properties and are stored on the event record.
// Basic event
await Wave.instance.track('product_viewed');
// Event with properties
await Wave.instance.track(
'transfer_completed',
properties: {
'amount': 2500000,
'currency': 'IDR',
'destination': 'BCA-1234567890',
},
idempotencyKey: 'TRX-7f3a9c2d', // prevents duplicate ingestion on retry
occurredAt: DateTime.now(), // converted to UTC automatically
);
{ "deduplicated": true } instead of creating a duplicate event.
Error handling
All SDK methods throw WaveException on failure. Wrap calls in try/catch where delivery is business-critical.
import 'package:flutter_wave_sdk/wave_flutter.dart';
try {
await Wave.instance.track('checkout_started', properties: {'cart_total': 150000});
} on WaveException catch (e) {
// e.message — human-readable error
// e.statusCode — HTTP status code (401, 422, 429, 5xx)
debugPrint('Wave error ${e.statusCode}: ${e.message}');
}
Complete integration example
A typical session lifecycle — init once, identify on login, register device and listen for token refresh, track throughout the session, deregister on logout.
// main.dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(); // your existing Firebase init — Wave reuses it
Wave.init(
baseUrl: 'https://wavebitz.com',
apiKey: 'wv_your_api_key_here',
);
runApp(const MyApp());
}
// auth_service.dart — after successful login
Future<void> onLoginSuccess(User user) async {
// 1. Identify the customer
await Wave.instance.identify(user.id, attributes: {
'name': user.name,
'email': user.email,
'tier': user.tier,
});
// 2. Register current push token
final token = await FirebaseMessaging.instance.getToken();
if (token != null) {
await Wave.instance.registerDevice(token);
}
// 3. Keep Wave in sync when FCM rotates the token
FirebaseMessaging.instance.onTokenRefresh.listen((newToken) {
Wave.instance.registerDevice(newToken);
});
// 4. Track the login event
await Wave.instance.track('customer_login');
}
// auth_service.dart — on logout (shared/public devices only)
Future<void> onLogout() async {
await Wave.instance.deregisterDevice();
}
// transfer_screen.dart — after a successful transfer
Future<void> onTransferComplete(Transfer tx) async {
await Wave.instance.track(
'transfer_completed',
properties: {
'amount': tx.amount,
'currency': 'IDR',
'destination': tx.destinationAccount,
'type': tx.type,
},
idempotencyKey: tx.referenceId,
occurredAt: tx.completedAt,
);
}
wave_delivery_id in the notification data payload to identify Wave-sourced messages. Wave never registers onBackgroundMessage, so your background handler is untouched.
Coexisting with existing FCM handlers
If your app already handles FCM messages, use wave_delivery_id in the payload to route Wave notifications separately.
Without this check, your foreground handler may double-display Wave notifications, and your tap handler may misroute them.
Foreground messages
The OS does not auto-display notifications when the app is in the foreground — your onMessage handler controls display. Skip Wave notifications here; the FCM payload already carries a notification block that the OS will show when the app is backgrounded.
FirebaseMessaging.onMessage.listen((message) {
if (message.data.containsKey('wave_delivery_id')) {
return; // Wave handles its own display — skip your custom logic
}
// Your existing foreground notification display
showLocalNotification(message);
});
Notification taps
Route by wave_delivery_id before your deep-link logic so Wave taps are recorded and your navigator isn't given a payload it doesn't recognise.
FirebaseMessaging.onMessageOpenedApp.listen((message) {
final deliveryId = message.data['wave_delivery_id'] as String?;
if (deliveryId != null) {
Wave.instance.trackPushOpened(deliveryId);
// Handle any Wave-specific deep link here if needed
return;
}
// Your existing deep-link / navigation logic
navigateTo(message.data['screen']);
});
Background messages
The OS shows Wave notifications automatically in the background — no action needed in your background handler unless you want to track the open.
Wave never registers its own onBackgroundMessage, so yours is always the only one.
@pragma('vm:entry-point')
Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
if (message.data.containsKey('wave_delivery_id')) {
return; // OS already showed it; tracking happens on tap via onMessageOpenedApp
}
// Your existing background handling
await handleBackgroundMessage(message);
}
void main() async {
FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler);
// ...
}
API reference
| Method | When to call |
|---|---|
| Wave.init(baseUrl, apiKey) | Once at app startup in main() |
| identify(customerId, attributes) | After login / session restore |
| registerDevice(fcmToken) | After identify, when token is available, and in onTokenRefresh |
| deregisterDevice() | On logout for shared/public devices — stops push delivery immediately |
| track(eventName, properties, idempotencyKey, occurredAt) | On every meaningful user action |
| trackPushOpened(deliveryId) | In onMessageOpenedApp when user taps a push notification |
| listExecutions() | To show journey history in-app |
| listNotifications() | To show notification history in-app |
| fetchInAppBanners() | Fetch pending in-app banner messages queued for this customer |
| markInAppDisplayed(id) | Report that a banner was shown — call when it renders on screen |
| markInAppClicked(id) | Report that the customer tapped the banner CTA |
| markInAppDismissed(id) | Report that the customer dismissed the banner without acting |
| Read API — server-side only (requires tenant API key) | |
| getCustomerFields(customerId) | All Calculated Fields for a customer with available/no_data status |
| getCustomerField(customerId, slug) | Single Calculated Field by slug |
| getCustomerMetrics(customerId) | All CI Metrics; -1.0 sentinel normalized to null |
| getCustomerMetric(customerId, slug) | Single CI Metric by slug |
| getCustomerSegments(customerId) | Segments the customer currently belongs to (live evaluation) |
| getCampaignDelivery(customerId, campaignId) | Latest delivery status per channel for a campaign |
| getCustomerExecutions(customerId, {limit}) | Journey execution history, most-recent first (default 50, max 100) |
React Native SDK
The Wave React Native SDK wraps the REST API for React Native and Expo apps — event tracking, customer identification, push device registration, and notification routing. Pure TypeScript, no native modules.
Download wave-react-native-sdk.zipStep 1 — Add the package
Extract the zip into your project, then add it as a local dependency in package.json:
"dependencies": {
"wave-react-native": "file:./wave-react-native"
}
Then run npm install (or yarn).
Step 2 — Initialise at app startup
Call Wave.init() once before mounting your root component. Your API key is in the Wave dashboard under Settings → Developer.
import { Wave } from 'wave-react-native';
Wave.init({
baseUrl: 'https://wavebitz.com',
apiKey: 'wv_your_api_key_here',
});
Step 3 — Identify the customer
Call after login or session restore. This links all subsequent track() calls to that customer's profile.
await Wave.instance.identify('usr_001', {
attributes: {
name: 'Budi Santoso',
email: 'budi@example.com',
tier: 'gold',
},
});
Step 4 — Register device and handle token refresh
Call after identify() whenever the FCM token is available. Listen for rotation to prevent silent delivery failures.
import messaging from '@react-native-firebase/messaging';
// Register current token
const token = await messaging().getToken();
await Wave.instance.registerDevice(token);
// Re-register on rotation
messaging().onTokenRefresh(newToken => {
Wave.instance.registerDevice(newToken);
});
Step 5 — Track events
await Wave.instance.track('transfer_completed', {
properties: {
amount: 2_500_000,
currency: 'IDR',
destination: 'BCA-1234567890',
},
idempotencyKey: 'TRX-7f3a9c2d',
});
Step 6 — Logout
For shared or public devices, deregister the token on logout to stop push delivery immediately. On the next login, call identify() and registerDevice() again.
// On logout
await Wave.instance.deregisterDevice();
// On next login
await Wave.instance.identify(newUser.id, { attributes: { ... } });
const token = await messaging().getToken();
await Wave.instance.registerDevice(token);
registerDevice() — that is the only coupling point. Your onMessage, onNotificationOpenedApp, and background handlers remain fully under your control.
Coexisting with existing FCM handlers
Use Wave.isWaveNotification(message.data) to route Wave notifications separately from your own. Without this guard your foreground handler may double-display Wave notifications and your tap handler may misroute them.
Foreground messages
messaging().onMessage(async message => {
if (Wave.isWaveNotification(message.data)) {
return; // Wave handles display — skip your custom logic
}
showLocalNotification(message);
});
Notification taps
messaging().onNotificationOpenedApp(async message => {
const id = Wave.deliveryId(message.data);
if (id) {
await Wave.instance.trackPushOpened(id);
return;
}
navigateTo(message.data.screen); // your existing deep-link logic
});
Background messages
messaging().setBackgroundMessageHandler(async message => {
if (Wave.isWaveNotification(message.data)) return;
await handleBackgroundMessage(message);
});
API reference
| Method | When to call |
|---|---|
| Wave.init(options) | Once at app startup |
| Wave.isWaveNotification(data) | In all FCM message handlers |
| Wave.deliveryId(data) | To extract the delivery ID for trackPushOpened |
| identify(customerId, options?) | After login / session restore |
| registerDevice(token, options?) | After identify, and in onTokenRefresh |
| deregisterDevice(token?) | On logout for shared/public devices |
| track(eventName, options?) | On every meaningful user action |
| trackPushOpened(deliveryId) | In onNotificationOpenedApp when user taps a Wave notification |
| fetchInAppBanners() | On app launch to pick up queued in-app messages |
| markInAppDisplayed(id) | When a banner renders on screen |
| markInAppClicked(id) | When the customer taps the banner CTA |
| markInAppDismissed(id) | When the customer dismisses the banner |
| Read API — server-side only (requires tenant API key) | |
| getCustomerFields(customerId) | All Calculated Fields for a customer with available/no_data status |
| getCustomerField(customerId, slug) | Single Calculated Field by slug |
| getCustomerMetrics(customerId) | All CI Metrics; -1.0 sentinel normalized to null |
| getCustomerMetric(customerId, slug) | Single CI Metric by slug |
| getCustomerSegments(customerId) | Segments the customer currently belongs to |
| getCampaignDelivery(customerId, campaignId) | Latest delivery status per channel for a campaign |
| getCustomerExecutions(customerId, {limit}) | Journey execution history, most-recent first (default 50, max 100) |
JavaScript SDK
A lightweight browser SDK for web apps — event tracking, customer identification, device registration, and web push. No build step required; drop in a single <script> tag and call the global WAVE object.
Step 1 — Include the script
Add the script tag before your closing </body>. The SDK is served from the WAVE platform directly — no npm package or bundler needed.
<script src="https://wavebitz.com/sdk/wave.js"></script>
Step 2 — Initialise once
Call Wave.init() once as early as possible — before any other SDK calls. Your API key is in the WAVE dashboard under Settings → General (Developer section).
Wave.init({
apiKey: 'wv_your_api_key_here',
baseUrl: 'https://wavebitz.com',
});
Step 3 — Identify the customer on login
Call Wave.identify() as soon as you know who the user is — after login or session restore. This links all subsequent track() calls to that customer's profile.
Wave.identify('usr_001', {
name: 'Budi Santoso',
email: 'budi@example.com',
tier: 'gold',
});
Step 4 — Track events
Call Wave.track() on every meaningful action. All extra fields go into properties and are stored on the event record.
// Basic event
Wave.track('page_viewed');
// Event with properties
Wave.track('transfer_completed', {
amount: 2500000,
currency: 'IDR',
destination: 'BCA-1234567890',
});
Step 5 — Enable web push (optional)
Web push requires a Firebase project. Host the service worker template on your own domain, then call Wave.enablePush() after identify(). The SDK handles permission request, token retrieval, and device registration automatically.
// 1. Download firebase-messaging-sw.js from https://wavebitz.com/sdk/firebase-messaging-sw.js,
// fill in your Firebase config, and host it at https://yourdomain.com/firebase-messaging-sw.js
// 2. Call after identify()
Wave.enablePush({
firebaseConfig: {
apiKey: 'AIza...',
authDomain: 'your-app.firebaseapp.com',
projectId: 'your-app',
storageBucket: 'your-app.appspot.com',
messagingSenderId: '123456789',
appId: '1:123...',
},
vapidKey: 'BNj...', // Firebase Console → Project Settings → Cloud Messaging
});
Step 6 — Report push opens (optional)
WAVE embeds a wave_delivery_id in every push notification's data payload. Call Wave.trackPushOpened() when the user clicks the notification so WAVE can record the engagement on the customer timeline.
The template already includes this handler. If you customise it, use event.waitUntil() so the browser keeps the service worker alive until the fetch completes.
// firebase-messaging-sw.js — notificationclick handler
// (already included in the downloaded template)
self.addEventListener('notificationclick', (event) => {
event.notification.close();
const deliveryId = event.notification.data?.wave_delivery_id;
event.waitUntil(
Promise.all([
// Wave.trackPushOpened() is not available in a service worker — POST directly
deliveryId
? fetch('https://wavebitz.com/api/v1/push/opened', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Api-Key': 'wv_your_key' },
body: JSON.stringify({ delivery_id: deliveryId }),
}).catch(() => {})
: Promise.resolve(),
// Focus existing tab or open a new one
clients.matchAll({ type: 'window', includeUncontrolled: true }).then((list) => {
const url = event.notification.data?.url || '/';
if (list.length > 0) return list[0].focus();
return clients.openWindow(url);
}),
])
);
});
Complete integration example
<script src="https://wavebitz.com/sdk/wave.js"></script>
<script>
// 1. Init once
Wave.init({ apiKey: 'wv_your_api_key_here', baseUrl: 'https://wavebitz.com' });
// 2. After the user logs in
function onLogin(user) {
Wave.identify(user.id, { name: user.name, email: user.email, tier: user.tier });
Wave.track('customer_login');
}
// 3. Track actions throughout the session
function onTransferComplete(tx) {
Wave.track('transfer_completed', {
amount: tx.amount,
currency: 'IDR',
destination: tx.destinationAccount,
});
}
</script>
API reference
| Method | When to call |
|---|---|
| Wave.init({ apiKey, baseUrl }) | Once, as early as possible on every page |
| Wave.identify(customerId, attributes?) | After login or session restore |
| Wave.track(eventName, properties?) | On every meaningful user action |
| Wave.trackPushOpened(deliveryId) | When a user clicks a push notification |
| Wave.registerDevice(token, platform?) | To register a push token manually (web / android / ios) |
| Wave.enablePush({ firebaseConfig, vapidKey }) | To enable browser push via Firebase; handles permission + registration |
| Wave.fetchInAppBanners() | On page load or app resume — returns array of pending in-app banner messages |
| Wave.markInAppDisplayed(id) | When a banner renders on screen |
| Wave.markInAppClicked(id) | When the customer taps the banner CTA |
| Wave.markInAppDismissed(id) | When the customer dismisses the banner without acting |
| Read API — server-side only (requires tenant API key) | |
| Wave.getCustomerFields(customerId) | All Calculated Fields for a customer with available/no_data status |
| Wave.getCustomerField(customerId, slug) | Single Calculated Field by slug |
| Wave.getCustomerMetrics(customerId) | All CI Metrics; -1.0 sentinel normalized to null |
| Wave.getCustomerMetric(customerId, slug) | Single CI Metric by slug |
| Wave.getCustomerSegments(customerId) | Segments the customer currently belongs to |
| Wave.getCampaignDelivery(customerId, campaignId) | Latest delivery status per channel for a campaign |
| Wave.getCustomerExecutions(customerId, options) | Journey execution history, most-recent first (default 50, max 100) |
Idempotency
WAVE prevents duplicate event ingestion using idempotency keys. If you send the same key twice, WAVE processes the first and silently ignores the second — returning { "deduplicated": true }.
When to use it
- Any time you retry a failed HTTP request (network timeout, 5xx response)
- Transfer / payment events — always include a transaction reference as the key
- Any event that should only trigger a journey once per occurrence
Key format
// Transaction ID from your system (best)
"idempotency_key": "TRX-2026-00182736"
// UUID generated client-side before the attempt
"idempotency_key": "7f3a9c2d-4e1b-4a8f-b3c1-2d9e0f1a5b6c"