Every endpoint, field, limit and error code, plus the Android SDK surface. Documented against the shipping implementation.
A small set of HTTPS endpoints. The SDK handles ingestion, signing and retries, so most integrations never call these. Call them for server to server tracking, or to build on the analytics data yourself.
Every endpoint below is served from one host.
https://api.measura.dev/v1/{function}Pointing at staging or a self-hosted project: override the endpoint in SDK configuration, do not patch the SDK.
Three mechanisms, three jobs. The API key never authenticates an event. The signing secret never appears in a header.
Issued per application. The format is the prefix msr_ followed by 64 hexadecimal characters.
GET /v1/resolve-key
X-Measura-Api-Key: msr_4f3c...e91aWe store a SHA-256 hash only. The plaintext is shown once and cannot be recovered, so a lost key is revoked and reissued.
Used once per SDK session, to exchange for the identifiers and the signing secret. It does not authenticate events.
Every event carries an HMAC-SHA256 signature computed with the signing secret from key resolution. This is what authenticates ingestion.
The canonical payload rules are strict:
signature field itself.import { createHmac } from 'node:crypto'
function sign(event, sdkSecret) {
const { signature: _omit, ...rest } = event
// Top level keys sorted, no whitespace.
const canonical = JSON.stringify(
Object.keys(rest).sort().reduce((acc, k) => {
acc[k] = rest[k]
return acc
}, {})
)
return createHmac('sha256', sdkSecret).update(canonical).digest('hex')
}Analytics and migration take a bearer session token from sign-in. Account identity comes from server-controlled token claims, so no parameter change can reach another tenant’s data.
GET /v1/analytics?resource=summary
Authorization: Bearer <session token>POST /v1/ingest
Submits one event or a batch of events for processing.
Send either a single event object, or an object with an events array. Cross origin requests are permitted from any origin.
{
"events": [
{
"event_type": "install",
"timestamp": 1754899200000,
"sent_at": 1754899201500,
"customer_id": "3f1c8a2e-9b47-4d1e-8a6f-2c5b9e7d1a03",
"app_id": "7d2e5b1a-4c93-4f8e-b6a1-9e3c7f2d5b84",
"event_id": "install_1754899200000_a3f9c1e7",
"sdk_version": "0.1.0",
"device_model": "Samsung SM-A245F",
"os_name": "android",
"os_version": "14",
"app_version": "2.3.1",
"device_fingerprint_components": {
"user_agent": "MeasuraSDK/0.1.0 (Android 14; SM-A245F) App/2.3.1",
"device_model": "Samsung SM-A245F",
"os_version": "14",
"screen_resolution": "1080x2340"
},
"referrer": "measura_click_id=8c1f...",
"signature": "9f2a...c74e"
}
]
}| Limit | Value | Response when exceeded |
|---|---|---|
| Events per request | 50 | 400 |
| Request body size | 256 KB | 413 |
| Signature age | plus or minus 10 minutes | Per event AUTH_ERROR |
| Monthly events | Per plan allowance | Per event QUOTA_EXCEEDED |
event_ typeenumclick, install, open, session_start, session_end, purchase, re_engagement, uninstall, custom.timestampintegercustomer_ iduuidapp_ iduuidevent_ idstring, 8 to 128 charssdk_ versionstringsignaturestring, exactly 64 hex charssent_ atintegertimestamp for the replay check, which lets you send historical events without tripping it.gaid, android_ idstringdevice_ fingerprint_ componentsobjectuser_agent, device_model, os_version and optional screen_resolution. The server adds the masked IP address before hashing.click_ iduuidcampaign_ iduuidchannelenumwhatsapp, ussd, qr_code, influencer, meta, google, tiktok, twitter, sms, email, organic, push_notification, referral, unknown.referrerstringdevice_ model, os_ name, os_ version, app_ version, screen_ resolutionstringevent_ propertiesobjectrevenuenumberrevenue property for you; a request built by hand must send it at the top level of the event.currencystringNGN or USD.{
"results": [
{
"status": "accepted",
"event_id": "install_1754899200000_a3f9c1e7",
"server_timestamp": 1786440001732
},
{
"status": "deduplicated",
"event_id": "open_1754899210000_b7d2f4a1",
"server_timestamp": 1786440001733
},
{
"error": "Invalid signature",
"code": "AUTH_ERROR"
}
],
"batch_size": 3
}Per event status is accepted, deduplicated or rejected. server_timestamp is Unix milliseconds.
| Status | Cause |
|---|---|
400 | Malformed JSON, empty array, or more than 50 events. |
405 | Method other than POST or OPTIONS. |
413 | Request body larger than 256 KB, or a gzip body that does not decompress. |
429 | Rate limited, with code RATE_LIMIT. Two limits apply: 3,000 requests a minute per IP address and 600 per application. Back off and retry. |
GET /v1/ingest/health
Liveness probe for the ingestion service.
{ "status": "ok", "service": "ingest" }This endpoint backs our public status page. It is safe to poll, but please keep the interval reasonable.
GET /v1/resolve-key
Exchanges an API key for the account identifiers and the event signing secret.
Called once at SDK initialisation and cached for the session.
{
"customer_id": "3f1c8a2e-9b47-4d1e-8a6f-2c5b9e7d1a03",
"app_id": "7d2e5b1a-4c93-4f8e-b6a1-9e3c7f2d5b84",
"sdk_secret": "b91f7c...4e2a",
"config": {
"tracking_enabled": true,
"sampling_rate": 1,
"batch_size": 50,
"flush_interval_ms": 30000,
"wifi_only_mode": true,
"suppress_on_low_battery": true,
"low_battery_threshold": 15,
"max_retry_attempts": 5,
"max_offline_queue_size": 10000,
"ingest_endpoint": null
}
}config is the remote SDK configuration (see Account operations). It is omitted if the lookup fails, and the SDK then keeps its built-in defaults.
| Status | Meaning |
|---|---|
401 | Missing, malformed, invalid or revoked key, or the account or app is inactive. |
405 | Method other than GET or OPTIONS. |
429 | More than 30 requests per minute from one IP address. |
GET /v1/deep-link/{slug}
Resolves a short link, records the click, and redirects to the correct destination for the device.
Links are issued on the functions host.
measura_click_id to the destination so the SDK can attribute the resulting install. When the link has an in-app path and the destination opens your app directly, rather than a store page, measura_path is appended too.referrer parameter, which survives the install and gives deterministic attribution. It carries the click ID and nothing else: deferred parameters are exchanged for it afterwards at /deferred-link, so your campaign data never appears in a URL that Play truncates and anyone holding the link can read.| Status | Meaning |
|---|---|
302 | Redirect to the resolved destination, or to the default fallback. |
404 | Slug shorter than three characters. |
429 | More than 120 requests per minute from one /24 network. |
GET /v1/analytics?resource={resource}
Reads aggregated attribution, cohort and fraud data for the signed in account.
resourcerequiredapp_ idoptionalteam_ idoptionaldate_ fromoptionaldate_ tooptional| Resource | Returns |
|---|---|
health | Liveness probe. Needs the apikey header but no signed-in session. |
summary | Totals for the period: installs, average confidence score, high confidence rate, and breakdowns by attribution model and channel. |
installs | Daily install counts by attribution model and channel, with average confidence. |
cohorts | Retention by install cohort and day offset. |
fraud | Fraud flags with the rule that fired, its version, the triggering signal values and the score. Limited to 500 rows. |
attribution_log | The glass box log: confidence breakdown, signals used, written reason, rejected candidates and postback status. Limited to 200 rows. |
revenue | Payment totals for the period per currency: gross, refunded and net, with the share of payments that matched a known device. Net per provider. |
roas | Per campaign, for the period: attributed installs; revenue per currency, split into in_app_revenue (SDK purchase events) and payment_revenue (payment webhooks, net of refunds and chargebacks); spend per currency; roas_by_currency (revenue divided by spend in the same currency); and roas when the spend is in one currency. Revenue is credited to the campaign of the paying device's most recent attributed install. Spend comes from a connected ad network or is entered in the dashboard. |
payments | Individual payments: provider, reference, amount, currency, whether it matched a device, and when it arrived. The raw provider payload is never returned. |
revenue returns a total for each currency separately rather than one figure. This is deliberate: adding NGN to USD produces a number that looks like revenue and means nothing. If you need a single figure, convert at a rate you control, at the moment you report, rather than relying on one baked in here.
{
"period": { "from": "2026-07-01", "to": "2026-07-31" },
"payment_count": 412,
"by_currency": {
"NGN": { "gross": 6120000, "refunded": 84000, "net": 6036000, "count": 388, "attributed": 5110500 },
"USD": { "gross": 2140, "refunded": 0, "net": 2140, "count": 24, "attributed": 1702 }
},
"by_provider": { "paystack": 6036000, "flutterwave": 2140 },
"attributed_count": 350,
"attribution_rate_pct": 85
}gross is payments only. Refunds, partial refunds, chargebacks and disputes are summed into refunded and subtracted to give net. attributed is the net amount tied to a known device and by_provider is net per provider. count and attributed_count count payments, and attribution_rate_pct, a whole number, is the share of payments matched to a device. A rate well below your install match rate usually means payments are arriving with an identifier the SDK never saw, rather than a fault in attribution.
{
"period": { "from": "2026-07-12", "to": "2026-08-11" },
"total_installs": 18432,
"avg_confidence_score": 81,
"high_confidence_rate_pct": 72.6,
"installs_by_model": {
"install_referrer": 8120,
"click_id": 4306,
"deterministic": 2988,
"probabilistic": 1442,
"organic": 1576
},
"installs_by_channel": {
"whatsapp": 6210,
"meta": 4980,
"qr_code": 2110,
"organic": 1576
}
}| Status | Meaning |
|---|---|
400 | Unknown resource, or an app_id or team_id outside your account. |
401 | Missing or invalid session. |
403 | No account linked to the signed in user. |
403 | Code FEATURE_NOT_AVAILABLE: your plan does not include this resource. cohorts needs the cohorts feature, fraud needs fraud detection, and revenue, payments and roas need ROAS. The body names the feature. |
405 | Method other than GET. |
Point your payment provider here and Measura ties each completed payment back to the campaign that produced the install. This covers revenue Play Billing never sees: bank transfer, USSD, and cards taken through a local processor.
POST /v1/payment-webhook?app_id={uuid}&provider={provider}
Receives a payment, refund, partial refund, chargeback or dispute from your provider and attributes it.
Both query parameters are required. provider must be one of paystack, flutterwave, stripe or generic. The body is your provider's own payload, forwarded unmodified.
Create the endpoint in the dashboard under Integrations. The dashboard shows the full URL to paste into your provider, and the signing secret once. It is not retrievable afterwards; revoke and create a new one if it is lost.
Header and algorithm depend on the provider. In every case the signature covers the exact bytes of the request body.
| Provider | Header | Expected value |
|---|---|---|
| paystack | x-paystack-signature | HMAC-SHA512 of the raw body, hex, keyed with your Paystack secret key |
| flutterwave | verif-hash | The shared secret verbatim, as configured in the Flutterwave dashboard |
| stripe | stripe-signature | HMAC-SHA256 of {timestamp}.{raw body}, hex, keyed with your Stripe webhook signing secret. The header carries the timestamp as t=...,v1=...; a request timestamped more than 5 minutes away from now is rejected. |
| generic | x-measura-signature | HMAC-SHA256 of the raw body, hex, keyed with the secret Measura issued |
Every recorded payment has an event_type: payment, refund, partial_refund, chargeback, or dispute. A reversal is a separate, immutable record linked to the original payment where Measura can determine the link - it never overwrites or deletes the original. Revenue reporting subtracts every reversal from gross to give net, rather than summing every row blindly.
| Provider | How a reversal is signalled |
|---|---|
| paystack | event: "refund.processed" |
| flutterwave | event: "refund.completed" |
| stripe | type: "charge.refunded" (full or partial, by amount) or type: "charge.dispute.created" |
| generic | You set event_type explicitly in your own payload. Defaults to payment if omitted - Measura has no vendor convention to infer it from for your own integration. |
Redelivering the same event - same provider, same reference, same event_type, scoped to your account - is safely ignored. Measura answers {"status": "duplicate"} with 200 rather than recording it twice. Providers retry on any non-2xx, so redelivery is expected and normal, not an error condition.
{
"status": "recorded",
"payment_event_id": "3f0a...",
"reference": "your-provider-reference",
"event_type": "payment",
"matched": true
}matched reports whether the payer was tied to a known device. A payment with matched: false is still recorded and still counts toward revenue; it simply has no install to attribute to.
| Status | Meaning |
|---|---|
200 recorded | Event stored and, for a payment, attribution attempted. |
200 duplicate | This provider reference and event_type were already recorded. Providers retry, so redelivery is expected and safe. |
200 ignored | A valid event that is not a recognised payment or reversal, for example a charge that failed. |
400 | Missing or malformed app_id, unknown provider, or a body that is not JSON. |
401 | Signature did not verify. |
404 | No active webhook for that app and provider. |
413 | Body exceeded the size limit. |
429 | Rate limit exceeded for this app_id. Retry after the interval in the Retry-After header. |
500 | The event could not be stored. Your provider will retry it. |
Paystack and Stripe send minor units (kobo, cents); Measura converts on the way in, so 499900 kobo is stored as 4999.00. Flutterwave sends major units already and is stored as received. Revenue totals are always reported per currency and never summed across them, because adding NGN to USD produces a number that means nothing.
This is the reason a mobile measurement partner exists. An ad network will not optimise a campaign it cannot measure, and it will not take your word for which installs it caused. So Measura attributes the install and reports it back to the network you configured, in that network's documented format.
Each network you configure has a postback_format, which decides the wire shape Measura sends:
| Format | Network | Required settings |
|---|---|---|
measura_canonical | Any network, via your own URL | None. Measura posts its own JSON to the URL you provide. |
meta_capi | Meta, WhatsApp Business | dataset_id, api_version, event_name |
google_ads_app | Google Ads app campaigns | link_id |
google_cm360 | Google Campaign Manager 360 | profile_id, floodlight_configuration_id, floodlight_activity_id |
snap_capi | Snapchat | pixel_id, event_name |
tiktok_events | TikTok | event_source_id, event_name |
whatsapp_business is not a separate wire format. Click-to-WhatsApp ad attribution is measured through Meta's own Conversions API, so a WhatsApp Business config uses meta_capi with WhatsApp-specific event naming, the same integration as a plain Meta config.
An install is sent only to the network whose link was clicked, taken from the click's channel. Installs with no ad click are not sent, except to google_ads_app, which receives every install because Google decides attribution itself and says so in its response. A config can apply to all apps or to one app; an app's own config wins. Step-by-step setup per network is in the Ad Network Setup guide.
When a config uses measura_canonical, this is exactly what arrives at your URL:
{
"event_type": "install",
"attribution_id": "b7e1...",
"install_id": "9a02...",
"installed_at": "2026-09-08T10:00:00.000Z",
"model": "last_click",
"confidence_score": 87,
"click_id": "c441...",
"campaign_id": "8f10...",
"gaid": "38400000-8cf0-11bd-b23e-10b96e40000d",
"measura_customer_id": "1d55...",
"timestamp": 1788800000000
}A purchase postback carries the same envelope with event_type: "purchase" plus value and currency. Purchase postbacks fire when a payment recorded through the payment webhook below resolves to a device that already has an attributed install.
A network config opts into install, purchase, or both. Choosing purchase is what makes revenue-based campaign optimisation possible on the network side, rather than install counts alone.
Delivery is attempted roughly every 60 seconds. A failure is classified before Measura decides whether to retry it:
| Failure | Classification | Behaviour |
|---|---|---|
| A vendor 4xx other than 429 (bad credential, malformed field, revoked token) | Permanent | No retry. The identical request cannot produce a different vendor decision. |
| HTTP 429, a 5xx, or a timeout | Transient | Retried with growing backoff (60s, 120s, 300s, 600s, then 900s), up to 5 attempts. |
| A missing credential or malformed setting | Permanent | No retry. Recorded distinctly so a half-configured network is not read as the network being down. |
After the final attempt, the attribution's postback status is marked failed for manual review. A successful delivery marks it sent.
SDK keys are normally created in the dashboard. This endpoint exists for teams that provision apps programmatically.
POST /v1/generate-key
Issues an SDK key for one of your applications.
{
"app_id": "0f7c...",
"label": "Production Android"
}label is optional and defaults to SDK Key. The app must belong to the account the session is linked to; another tenant's app_id is answered 404, exactly as if it did not exist.
{
"api_key": "msr_4f3c...e91a",
"key_id": "5b1e...",
"label": "Production Android",
"app_id": "0f7c...",
"warning": "Store this now. It is shown once and cannot be recovered."
}| Status | Meaning |
|---|---|
400 | Missing or malformed app_id. |
401 | Missing, malformed or expired session. |
403 | The session has no linked account, or you do not have the developer role. |
404 | The app is not in your account. |
409 | The app is deleted or deactivated. |
500 | The key could not be issued. Safe to retry. |
These are the main procedures the dashboard itself calls. They are ordinary PostgREST procedure calls, so anything that speaks HTTP can drive them: provisioning a new app, flipping the SDK kill switch during an incident, or rotating a payment webhook secret from a deploy script.
POST /rest/v1/rpc/{procedure}
Account scoped operations, called the same way the dashboard calls them.
The SDK asks the server for its settings, so these take effect on the next configuration fetch without an app release. This is the lever to reach for during an incident rather than shipping a hotfix to the store.
| Procedure | Effect |
|---|---|
upsert_sdk_config | Overrides tracking_enabled, sampling_rate, wifi_only_mode, batch_size, flush_interval_ms or suppress_on_low_battery for your account, with an optional p_notes recording why. Any argument left null inherits the platform default rather than resetting it. Owner only. |
clear_sdk_config | Removes every override, returning the account to platform defaults. Owner only. |
curl -X POST "$MEASURA_URL/rest/v1/rpc/upsert_sdk_config" \
-H "apikey: $ANON_KEY" \
-H "Authorization: Bearer $SESSION_JWT" \
-H "Content-Type: application/json" \
-d '{"p_tracking_enabled": false}'| Procedure | Effect |
|---|---|
list_payment_webhooks | Your webhooks, with the app, provider, label and last use. The signing secret is never returned. |
create_payment_webhook | Creates one for an app you own and returns id, provider, signing_secret and webhook_url. webhook_url is a path: prefix it with the API base URL above. The secret is shown once and cannot be retrieved later. Creating a second webhook for the same app and provider deactivates the first. Needs the developer role or above. |
revoke_payment_webhook | Deactivates a webhook. Deliveries to it stop being accepted. |
reactivate_payment_webhook | Turns a revoked webhook back on with its existing secret. |
delete_payment_webhook | Removes a webhook permanently. |
create_payment_webhook checks the app belongs to you before issuing anything, so a valid session cannot mint a webhook against another account. Paystack, Flutterwave and Stripe require the secret from their own dashboard; generic generates one for you.
An account holds one or more teams, and apps belong to a team. Creating a team and inviting someone are separate operations.
| Procedure | Effect |
|---|---|
create_team(p_name, p_avatar) | Creates a team in your account and returns its id. |
rename_team(p_team_id, p_name) | Renames a team. |
archive_team(p_team_id) | Archives a team. The default team cannot be archived. |
create_invitation(p_team_id, p_email, p_role, p_expires_days) | Invites someone by email with a role of owner, developer, marketer or viewer. Returns invitation_id, token and expires_at. They do not need an account yet. |
accept_invitation(p_token) | Called by the invited person, signed in, to join. Returns the team id. |
revoke_invitation(p_invitation_id) | Withdraws an invitation that has not been accepted. |
update_member_role(p_team_id, p_user_id, p_role) | Changes a member's role. |
team_remove_member(p_team_id, p_user_id) | Removes a member. The last owner cannot be removed, since that would leave the team with nobody able to invite anyone back. |
Roles are checked in the database. A session without the required role is refused with an error rather than silently doing nothing.
| Procedure | Effect |
|---|---|
create_app(p_name, p_bundle_id, p_team_id) | Creates an Android app in a team and returns its id. Counts against your plan's app limit. |
revoke_api_key(p_key_id) | Stops a key working immediately. A lost key is revoked, then a new one issued with generate-key. |
delete_api_key(p_key_id) | Removes a revoked key from the list. |
create_deep_link(p_app_id, p_slug, p_fallback_url, p_channel, p_android_url, p_ussd_fallback, p_campaign_id, p_deep_link_path, p_deferred_params) | Creates a short link. Only p_app_id, p_slug and p_fallback_url are required. |
set_link_active(p_link_id, p_active) | Pauses or resumes a link. A paused link records no clicks and sends visitors to Measura's default page. |
set_ad_network_credential(p_config_id, p_credential) | Stores the access token a native postback format needs. Write only: it is never returned. |
Everything Measura holds for your account, in one JSON document. There is no notice period, no support ticket, and no export fee. If you decide to leave, your attribution history leaves with you.
POST /rest/v1/rpc/export_my_account_data
Returns your apps, campaigns, links, installs, attributions, events, fraud flags and payments.
curl -X POST "$MEASURA_URL/rest/v1/rpc/export_my_account_data" \
-H "apikey: $ANON_KEY" \
-H "Authorization: Bearer $SESSION_JWT" \
-H "Content-Type: application/json" \
-d '{"p_max_rows": 50000}' > measura-export.jsonp_max_rows caps rows per table and defaults to 50,000. It is clamped to 200,000: a single response has to fit in memory, and an uncapped export of a large account would not. For accounts beyond that, export in date ranges or ask us for a bulk dump.
The document includes a totals block per table, so you can confirm the export is complete before relying on it. If a count there equals your cap, the table was truncated and you should re-export that range with a higher one.
Three endpoints, called in order, import historical data from another attribution platform. The Migration Guide covers the column mappings and file preparation in detail.
POST /v1/migration-importer/start
Creates an import job and returns the column mapping that will be applied.
// Request
{ "app_id": "7d2e5b1a-...", "source_platform": "appsflyer" }
// 200 OK
{
"job_id": "c4a9...",
"message": "Import job created",
"column_mapping": { "Install Time": "installed_at", "Advertising ID": "gaid" }
}POST /v1/migration-importer/upload?job_id={id}
Uploads the data file and begins processing.
Accepts application/json, text/csv or text/plain. Maximum 5 MB and 50,000 rows.
GET /v1/migration-importer/status?job_id={id}
Reports progress and any row level errors.
{
"job_id": "c4a9...",
"status": "processing",
"total_rows": 24500,
"processed_rows": 11200,
"progress_pct": 45,
"errors": [],
"started_at": "2026-08-11T09:04:22.010Z",
"completed_at": null
}| Status | Meaning |
|---|---|
400 | Missing fields, unsupported platform, or job not in a pending state. |
401 | Missing or invalid session. |
403 | The application does not belong to your account. |
403 | Code FEATURE_NOT_AVAILABLE: your plan does not include the importer. |
404 | Job not found. |
413 | File over 5 MB or over 50,000 rows. |
415 | Unsupported content type. |
500 | A fault on our side. Safe to retry. |
Ingestion errors, and the 403 a plan gate returns, carry a machine readable code. Other endpoints return {"error": "..."} with the HTTP status alone.
| Code | Meaning | What to do |
|---|---|---|
VALIDATION_ERROR | A field is missing, malformed or out of range. | Fix the payload. Retrying unchanged will fail again. |
AUTH_ERROR | Bad signature, unknown account, inactive account, or outside the replay window. | Verify the canonical payload rules and check for clock drift. |
RATE_LIMIT | Too many requests. | Back off exponentially and retry. |
FEATURE_NOT_AVAILABLE | Your plan does not include this feature. HTTP 403; the body names the feature. | Upgrade the plan. Retrying will not help. |
QUOTA_EXCEEDED | The monthly event allowance is exhausted. | Upgrade the plan or wait for the monthly reset. Retrying will not help. |
INTERNAL_ERROR | Something failed on our side. | Retry with backoff. If it persists, contact support. |
Kotlin, minimum SDK 21. The release archive measures under 100 KB gzipped, enforced by a build gate at the same figure, so the number here cannot drift from the shipped artifact. Integrating it grows a minified APK by about 312 KB, since the archive excludes AndroidX WorkManager and the Room storage behind its job queue. Apps already using Kotlin coroutines see less.
import dev.measura.sdk.Measura
import dev.measura.sdk.core.MeasuraConfig
Measura.init(
context = applicationContext,
apiKey = "msr_4f3c...e91a",
config = MeasuraConfig(
batchSize = 50,
flushIntervalMs = 30_000L,
wifiOnlyMode = true,
maxOfflineQueueSize = 10_000
)
)init(context, apiKey, config)UnitApplication.onCreate.trackEvent(eventType, properties)UnitString or an EventType. A string that is not one of the standard event types is sent as custom, with your name in custom_event_name. Throws IllegalStateException if the SDK is not initialised.identify(userId, traits)Unitcustom event with custom_event_name set to identify. From Java, pass the traits map explicitly (an empty map is fine): this method has no Java overload without it.setDeferredDeepLinkListener(listener)Unitnull to clear a previously registered listener. The listener is a MeasuraDeferredLinkListener whose onDeferredDeepLink(link) receives a MeasuraDeferredLink with path, params, clickId, slug, campaignId, channel and param(key).handleDeepLink(uri)String?re_engagement against the click that brought the user back and returns the in-app path from measura_path, or null when the URL carries none. Records nothing for a URL without measura_click_id, or while tracking is disabled.disableTracking() / enableTracking()UnitsetPushToken(token)UnitonNewToken and once at startup.setIntegrityToken(token)UnitMeasura.CLOUD_PROJECT_NUMBER, or the token cannot be decoded.flush()UnitisInitialised()Boolean| Option | Default | Effect |
|---|---|---|
| batchSize | 50 | Events buffered before a send is triggered. |
| flushIntervalMs | 30000 | Timer driven flush interval. |
| wifiOnlyMode | true | Hold events until Wi-Fi is available. See the note below. |
| suppressOnLowBattery | true | Defer sending on low battery. |
| lowBatteryThreshold | 15 | Battery percentage below which sending is deferred. |
| maxRetryAttempts | 5 | Retries before the batch is written back to disk. |
| maxOfflineQueueSize | 10000 | Persistent queue depth. Oldest events drop first. |
| ingestEndpoint | api.measura.dev/v1/ingest | Override for self hosted or staging. Must be https. |
| logLevel | NONE | NONE, ERROR, DEBUG or VERBOSE. |
| collectAdvertisingId | true | Read the Google advertising ID when the user allows it. Set false to never read it. |
| customerId, appId | null | Optional. Normally resolved from the API key; set only if you already hold them. |
There is no tracking permission requirement. Install attribution rides the Play Install Referrer through the store install automatically, and deferred deep link delivery is push based: setDeferredDeepLinkListener is called for you, at most once, if the install came from a Measura link with a configured destination. The one URL you hand the SDK yourself is a link that opens an app already installed:
intent?.data?.let { uri ->
Measura.handleDeepLink(uri)?.let { path -> navigateTo(path) }
}A failed send is retried after 1, 2, 4 and 8 seconds and then written to disk, never dropped. A 4xx other than 408 or 429 is not retried: the batch is written to disk at once, since the same bytes would get the same answer.
The SDK is built to survive poor connectivity without losing events. Understanding the deferral rules explains most reports of missing data.
In every case below the event is written to persistent storage rather than dropped.
wifiOnlyMode is on and the device is on cellular. This is the default.| Behaviour | Value |
|---|---|
| Persistent store | SQLite database on the device |
| Queue depth | 10,000 events |
| Overflow policy | Oldest dropped first |
| Batch size | 50 events |
| Flush interval | 30 seconds |
| Retry attempts | 5 |
| Backoff | Doubling from 1 second |
| Compression | gzip |
When retries are exhausted the batch returns to persistent storage and is retried in a later session rather than discarded.