← Home
WAVE v2

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.

Source Plane

Receives events from all adapters.

Integrations → Sources · Imports · Webhooks

Knowledge Plane

Builds the Event Catalog, computes Metrics, maintains Classifications, and updates Segments automatically.

Audience → Customers · Segments  ·  Data → Events · Event Catalog  ·  Intelligence → Metrics · Classifications

Execution Plane

Evaluates journeys and campaigns, tracks Goals, delivers push, email, WhatsApp, and in-app banners.

Automation → Journeys · Campaigns · Automation Rules · Scheduled Events

Assurance Plane

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. 1

    Get your API key

    Log in to WAVE → Settings → General → Developer section → copy the API key. Send it as the X-Api-Key header on every request.

  2. 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. 3

    Check the Event Catalog

    WAVE automatically discovers event types. Open Data → Event Catalog — you'll see app_opened appear with its properties. No configuration needed.

  4. 4

    Register a push device

    From your mobile app, call POST /api/v1/devices/register with the FCM token. This links the device to the customer so WAVE can deliver push notifications.

  5. 5

    Create a Journey

    Go to Automation → Journeys → New Journey. Set the trigger to app_opened, add a push action, and click Publish.

Platform Guide

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.

Assurance Plane: Analytics shows aggregate counts. For a per-customer, per-execution breakdown — what event fired it, what condition matched, whether the Goal was achieved — go to Automation → Executions.

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.

Events vs Event Catalog: Events is the raw history (individual rows). The Catalog is the schema layer (unique event types + their observed property keys). They're separate pages under Data.

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.

Note: The catalog reflects reality. If a property appears in some events but not others, it still appears in the catalog — but journey conditions that depend on it will simply not match for customers whose event didn't include it.

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_score0–100 score based on recency, frequency, and diversity of events
days_since_last_eventDays since the customer's most recent event
total_eventsTotal number of events ingested for this customer
events_last_30dEvent count in the last 30 days
Dormancy detection: 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_tiertotal_spend_30d > 5,000,000 → VIP · total_spend_30d > 1,000,000 → Regular · otherwise → New
activity_statusdays_since_last_event < 7 → Active · days_since_last_event < 30 → Passive · otherwise → Dormant
risk_levelfailed_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.

Transition Events: When a customer's Classification value changes — for example, from Regular to VIP — WAVE automatically emits a 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

  1. Go to Audience → Segments → New Segment
  2. Name the segment (e.g. High-Value Active Customers)
  3. Add conditions: total_spend_30d > 1000000 AND days_since_last_event < 14
  4. 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

T

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.

W

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.

D

Wait (optional)

Wait before taking action. Example: send the notification 30 minutes after the trigger. The execution is queued and fires automatically.

M

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 }}.

G

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
AchievedThe customer performed the goal event within the time window
PendingThe time window is still open
Timed outThe window expired without the customer completing the goal
NoneThis journey has no goal defined
Note: Draft journeys never execute. A journey must be explicitly published to become active.

Campaigns

Automation → Campaigns

Campaigns are one-time broadcasts to a segment. Unlike journeys, they are triggered manually and are not repeated.

Creating a Campaign

  1. Go to Automation → Campaigns → New Campaign
  2. Name the campaign and choose channel: Push or Email
  3. Choose target: All Customers or a specific Segment
  4. Compose the message
  5. Click Launch — WAVE delivers to all qualifying customers immediately
Email campaigns require SMTP to be configured under Settings → Email. Push campaigns require at least one device registered for the customer.

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-basedFires when a named event arrives, with an optional property condition
DormancyFires when a customer has not been seen for N days
Emit Scheduled EventConverts the rule match into a synthetic event (e.g. customer_dormant_detected)
Event SequenceFires 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
Source: Scheduled Events appear under Integrations → Sources with the type 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
Why this matters: If a customer asks "why did I get this notification?", the Executions page gives you a complete, timestamped explanation — including the goal outcome. No guessing.

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 APIapiBackend services sending events in real-time
Flutter SDKsdkMobile apps (Android, iOS)
JavaScript SDKsdkWeb apps and browser-based frontends
CSV Event ImportscsvHistorical data migration or batch event uploads
WebhookswebhookLegacy or config-only systems that can only be given a URL — use the REST API instead if your backend can make HTTP calls
Scheduled EventsschedulerTime-based synthetic events: dormancy detection, birthdays, inactivity
Sources vs Imports: Sources track behavior events only. Customer profile imports (CSV of customer names, emails, attributes) appear under Integrations → Imports, not Sources.

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 CSVIntegrations → Imports → Upload Customer CSVCreates or updates customer profiles (name, email, attributes)
Event CSVIntegrations → Imports → Upload Event CSVIngests 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.

  1. Go to Integrations → Webhooks → Add Endpoint
  2. Give it a name (e.g. "Core Banking") — this becomes the source_slug
  3. Save. Copy the signing secret that appears — it is shown only once.
  4. 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.

API vs Webhook — which to use?
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 Hoste.g. smtp.gmail.com
Port587 (TLS) or 465 (SSL)
UsernameYour SMTP login (usually your email address)
PasswordFor Gmail: use an App Password, not your Google password
From AddressThe sender address recipients will see
From NameThe 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.

  1. Go to Firebase Console → your project → Project Settings → Service Accounts
  2. Click Generate new private key → download the JSON file
  3. Open the file, copy its full contents, paste into Settings → Push Notifications → Service Account JSON
  4. Save. Your project ID will appear confirming the credentials are active.
Developer Guide

Downloads

Everything you need to integrate WAVE into your application.

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_idstringYesYour unique identifier for this customer
event_namestringYesEvent name in snake_case (e.g. transfer_completed)
propertiesobjectNoArbitrary key/value pairs describing the event
occurred_atISO 8601NoBusiness time of the event. Defaults to now.
idempotency_keystringNoUnique key to prevent duplicate ingestion on retry
Timestamps: Always send 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
eventsarrayYesArray of event objects. Min 1, max 1,000 per request.
events[].customer_idstringYesYour unique identifier for this customer
events[].event_namestringYesEvent name in snake_case
events[].propertiesobjectNoArbitrary key/value pairs
events[].occurred_atISO 8601NoDefaults to ingestion time if omitted
events[].idempotency_keystringNoPer-event deduplication key
import_idUUIDNoLinks 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.

Idempotency-Key header: If a batch with the same key was already accepted, the original batch_id is returned immediately — no new batch is created and admission control is not checked. Safe to retry on network failures.
Backpressure — 503 Service Unavailable: If the ingestion queue is overloaded, the API returns 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
modestringNolive (default) or historical. Use historical for backfills.
automation_policystringNotrigger (default for live) or suppress. Historical defaults to suppress.
ci_policystringNorecompute (default) or defer. Defer CI until after the full import completes.
labelstringNoHuman-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
livetriggerrecomputeNormal real-time behavior — journeys fire, CI updates per event
historicalsuppressrecomputeJourneys suppressed, CI updated per event (slower for large imports)
historicalsuppressdeferJourneys 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
currentCI is up to date (live mode or historical+recompute)
deferredImport still in progress; CI will be reconciled after completion
pending_reconciliationImport completed; CI reconciliation not yet run
reconciledCI 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.

CI reconciliation for deferred imports: After 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_idYesYour unique customer identifier
event_nameYesEvent name in snake_case
occurred_atNoISO 8601 timestamp. Defaults to import time.
[any other column]NoAdditional 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
Idempotency: Duplicate rows in a CSV are automatically detected and skipped. Re-uploading a file is safe. Every import is logged in Integrations → Imports.

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.

When to use this vs the Events API: Use the Events API if your backend can make HTTP calls — it is simpler (just an API key header, no HMAC 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.

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');
Important: Sign the raw body bytes exactly as sent — do not re-serialise the parsed JSON. Any whitespace difference will break signature verification.

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.

Server-side only. The Read API uses your tenant API key. Call it from your backend — never from client-side code or a mobile app where the key would be exposed.
Endpoint Returns
GET /api/v1/customers/{id}/intelligence/fieldsAll Calculated Fields
GET /api/v1/customers/{id}/intelligence/fields/{slug}Single Calculated Field
GET /api/v1/customers/{id}/intelligence/metricsAll CI Metrics
GET /api/v1/customers/{id}/intelligence/metrics/{slug}Single CI Metric
GET /api/v1/customers/{id}/segmentsSegment membership
GET /api/v1/customers/{id}/campaigns/{cid}/deliveryCampaign delivery status
GET /api/v1/customers/{id}/executionsJourney execution history
POST /api/v1/customers/identify/batchUpsert up to 100 customer profiles
GET /api/v1/journeys/{id}/deliveriesAll 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"
    }
  ]
}
statusMeaning
queuedAccepted, waiting for a worker
sentHanded off to the provider; receipt not yet confirmed
deliveredProvider confirmed delivery (push / WhatsApp)
openedCustomer opened (email tracking pixel)
failedDelivery failed; see failure_reason
skippedSuppressed (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"
    }
  ]
}
statusMeaning
pendingQueued, not yet fired
waitingPaused at a Wait-for-Event step
firingCurrently being processed
sentDelivered to the channel
failedProcessing failed
exitedLeft the journey (goal met or branch exit)
skippedSuppressed; 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

HTTPCause
401Missing or invalid X-Api-Key
404Customer or campaign not found in this tenant
422Invalid identifier (empty customer_id, campaign_id ≤ 0, etc.)
429Rate 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.

Path A — No FCM yet

1. Create a Firebase project

  1. Go to console.firebase.google.com → Create project
  2. In Project Settings → Cloud Messaging confirm Cloud Messaging is enabled
  3. 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 a firebaseConfig object (Web)

2. Upload your service account to Wave

  1. Firebase Console → Project Settings → Service accounts → Generate new private key → download the JSON
  2. 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.

Flutter
// 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));
React Native
// 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));
Done. Wave can now deliver push to this customer's device. See the Flutter SDK or React Native SDK sections for the full API reference.

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

  1. Firebase Console → Project Settings → Service accounts → Generate new private key → download the JSON
  2. 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.

Flutter
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
React Native
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.

Flutter
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']);
});
React Native
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);
});
Done. Your existing Firebase setup, tokens, and handlers are unchanged — Wave rides alongside them. See the Flutter SDK or React Native SDK sections for the full API reference.

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.zip

Step 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);
});
Note: 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.
    }
  }
});
Note: This records engagement in the customer timeline and on the Journey Executions page alongside Opened and Clicked timestamps. It does not fire a new WAVE event — use 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
);
Idempotency key: Always pass a unique transaction reference for payment and transfer events. If the request times out and you retry, Wave will deduplicate and return { "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,
  );
}
Already using Firebase? Wave does not re-initialise Firebase or replace your existing notification handlers. It reads the FCM token you provide and uses 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.zip

Step 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);
Already using Firebase? Wave does not re-initialise Firebase or replace your existing notification handlers. Pass the FCM token to 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
});
VAPID key: Found in Firebase Console → Project Settings → Cloud Messaging → Web push certificates. Generate one if none exists.

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"
Rule: Generate the key before the first attempt. Reuse it on every retry. Never generate a new key for a retry.