Browse documentation

API Reference

Every endpoint, field, limit and error code, plus the Android SDK surface. Documented against the shipping implementation.

Overview

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.

Base URL

Every endpoint below is served from one host.

Base URLtext
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.

Conventions

  • All request and response bodies are JSON, encoded as UTF-8.
  • Timestamps are Unix milliseconds unless a field is documented as an ISO date.
  • Identifiers described as UUIDs are validated as version 4.
  • Ingestion accepts gzipped request bodies, which the SDKs use by default.

Authentication

Three mechanisms, three jobs. The API key never authenticates an event. The signing secret never appears in a header.

1. API key

Issued per application. The format is the prefix msr_ followed by 64 hexadecimal characters.

Key resolution requesthttp
GET /v1/resolve-key
X-Measura-Api-Key: msr_4f3c...e91a

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

2. Event signature

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:

  • Serialise the event object as JSON with no whitespace.
  • Sort top level keys alphabetically.
  • Exclude the signature field itself.
  • Compute HMAC-SHA256 over that string with the signing secret, and hex encode the result. It must be exactly 64 characters.
Signing an eventjavascript
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')
}

3. Dashboard session token

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.

Authenticated dashboard requesthttp
GET /v1/analytics?resource=summary
Authorization: Bearer <session token>

Event ingestion

POST /v1/ingest

Submits one event or a batch of events for processing.

Auth
Per event HMAC signature. No bearer token.

Request

Send either a single event object, or an object with an events array. Cross origin requests are permitted from any origin.

POST /v1/ingestjson
{
  "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"
    }
  ]
}

Limits

LimitValueResponse when exceeded
Events per request50400
Request body size256 KB413
Signature ageplus or minus 10 minutesPer event AUTH_ERROR
Monthly eventsPer plan allowancePer event QUOTA_EXCEEDED

Event fields

Required

event_typeenum
One of click, install, open, session_start, session_end, purchase, re_engagement, uninstall, custom.
timestampinteger
Unix milliseconds. When the event occurred on the device.
customer_iduuid
From key resolution.
app_iduuid
From key resolution.
event_idstring, 8 to 128 chars
Your idempotency key. Resubmitting the same value is deduplicated rather than double counted.
sdk_versionstring
Identifies the sending client.
signaturestring, exactly 64 hex chars
HMAC-SHA256 as described in Authentication.

Optional

sent_atinteger
Unix milliseconds at signing time. Preferred over timestamp for the replay check, which lets you send historical events without tripping it.
gaid, android_idstring
Device identifiers. An all zero UUID, which signals limited ad tracking, is discarded after signature verification.
device_fingerprint_componentsobject
user_agent, device_model, os_version and optional screen_resolution. The server adds the masked IP address before hashing.
click_iduuid
The Measura click identifier, when known.
campaign_iduuid
Associates the event with a campaign.
channelenum
One of whatsapp, ussd, qr_code, influencer, meta, google, tiktok, twitter, sms, email, organic, push_notification, referral, unknown.
referrerstring
Install referrer string or source URL.
device_model, os_name, os_version, app_version, screen_resolutionstring
Device and application context.
event_propertiesobject
Free form properties you attach. Not inspected by Measura, so do not place sensitive personal data here.
revenuenumber
Must be zero or greater. The Android SDK lifts it out of a revenue property for you; a request built by hand must send it at the top level of the event.
currencystring
ISO 4217 code, for example NGN or USD.

Response

200 OKjson
{
  "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.

Top level errors

StatusCause
400Malformed JSON, empty array, or more than 50 events.
405Method other than POST or OPTIONS.
413Request body larger than 256 KB, or a gzip body that does not decompress.
429Rate limited, with code RATE_LIMIT. Two limits apply: 3,000 requests a minute per IP address and 600 per application. Back off and retry.

Health check

GET /v1/ingest/health

Liveness probe for the ingestion service.

Auth
None.
200 OKjson
{ "status": "ok", "service": "ingest" }

This endpoint backs our public status page. It is safe to poll, but please keep the interval reasonable.

Key resolution

GET /v1/resolve-key

Exchanges an API key for the account identifiers and the event signing secret.

Auth
X-Measura-Api-Key header. Minimum 16 characters.

Called once at SDK initialisation and cached for the session.

200 OKjson
{
  "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.

StatusMeaning
401Missing, malformed, invalid or revoked key, or the account or app is inactive.
405Method other than GET or OPTIONS.
429More than 30 requests per minute from one IP address.

Analytics

GET /v1/analytics?resource={resource}

Reads aggregated attribution, cohort and fraud data for the signed in account.

Auth
Bearer session token. The account is taken from token claims, never from a parameter.

Parameters

resourcerequired
One of the resources listed below.
app_idoptional
Restricts results to one application.
team_idoptional
Restricts results to one team's applications. An app or team outside your account is a 400.
date_fromoptional
ISO date. Defaults to 30 days ago.
date_tooptional
ISO date. Defaults to today.

Resources

ResourceReturns
healthLiveness probe. Needs the apikey header but no signed-in session.
summaryTotals for the period: installs, average confidence score, high confidence rate, and breakdowns by attribution model and channel.
installsDaily install counts by attribution model and channel, with average confidence.
cohortsRetention by install cohort and day offset.
fraudFraud flags with the rule that fired, its version, the triggering signal values and the score. Limited to 500 rows.
attribution_logThe glass box log: confidence breakdown, signals used, written reason, rejected candidates and postback status. Limited to 200 rows.
revenuePayment totals for the period per currency: gross, refunded and net, with the share of payments that matched a known device. Net per provider.
roasPer 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.
paymentsIndividual payments: provider, reference, amount, currency, whether it matched a device, and when it arrived. The raw provider payload is never returned.

Revenue is reported per currency, never summed

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.

GET ?resource=revenuejson
{
  "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.

GET ?resource=summaryjson
{
  "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
  }
}
StatusMeaning
400Unknown resource, or an app_id or team_id outside your account.
401Missing or invalid session.
403No account linked to the signed in user.
403Code 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.
405Method other than GET.

Payment webhooks

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.

Auth
The provider's signature over the raw request body. No JWT.

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.

Creating a webhook

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.

Signature verification

Header and algorithm depend on the provider. In every case the signature covers the exact bytes of the request body.

ProviderHeaderExpected value
paystackx-paystack-signatureHMAC-SHA512 of the raw body, hex, keyed with your Paystack secret key
flutterwaveverif-hashThe shared secret verbatim, as configured in the Flutterwave dashboard
stripestripe-signatureHMAC-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.
genericx-measura-signatureHMAC-SHA256 of the raw body, hex, keyed with the secret Measura issued

Event types

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.

ProviderHow a reversal is signalled
paystackevent: "refund.processed"
flutterwaveevent: "refund.completed"
stripetype: "charge.refunded" (full or partial, by amount) or type: "charge.dispute.created"
genericYou 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.

Idempotency

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.

Responses

200 OK, payment recordedjson
{
  "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.

StatusMeaning
200 recordedEvent stored and, for a payment, attribution attempted.
200 duplicateThis provider reference and event_type were already recorded. Providers retry, so redelivery is expected and safe.
200 ignoredA valid event that is not a recognised payment or reversal, for example a charge that failed.
400Missing or malformed app_id, unknown provider, or a body that is not JSON.
401Signature did not verify.
404No active webhook for that app and provider.
413Body exceeded the size limit.
429Rate limit exceeded for this app_id. Retry after the interval in the Retry-After header.
500The event could not be stored. Your provider will retry it.

Amounts and currency

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.

Ad network postbacks

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.

Configuring a network

Each network you configure has a postback_format, which decides the wire shape Measura sends:

FormatNetworkRequired settings
measura_canonicalAny network, via your own URLNone. Measura posts its own JSON to the URL you provide.
meta_capiMeta, WhatsApp Businessdataset_id, api_version, event_name
google_ads_appGoogle Ads app campaignslink_id
google_cm360Google Campaign Manager 360profile_id, floodlight_configuration_id, floodlight_activity_id
snap_capiSnapchatpixel_id, event_name
tiktok_eventsTikTokevent_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.

Which network receives an install

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.

Canonical payload shape

When a config uses measura_canonical, this is exactly what arrives at your URL:

POST to your postback_urljson
{
  "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.

Event types

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 and retries

Delivery is attempted roughly every 60 seconds. A failure is classified before Measura decides whether to retry it:

FailureClassificationBehaviour
A vendor 4xx other than 429 (bad credential, malformed field, revoked token)PermanentNo retry. The identical request cannot produce a different vendor decision.
HTTP 429, a 5xx, or a timeoutTransientRetried with growing backoff (60s, 120s, 300s, 600s, then 900s), up to 5 attempts.
A missing credential or malformed settingPermanentNo 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.

Creating an API key

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.

Auth
Bearer, a signed-in user session. Not an SDK key.
Requestjson
{
  "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.

200 OKjson
{
  "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."
}
StatusMeaning
400Missing or malformed app_id.
401Missing, malformed or expired session.
403The session has no linked account, or you do not have the developer role.
404The app is not in your account.
409The app is deleted or deactivated.
500The key could not be issued. Safe to retry.

Account operations

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.

Auth
Bearer, a signed-in user session, plus the apikey header.

SDK configuration

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.

ProcedureEffect
upsert_sdk_configOverrides 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_configRemoves every override, returning the account to platform defaults. Owner only.
Stop collection for your accountbash
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}'

Payment webhooks

ProcedureEffect
list_payment_webhooksYour webhooks, with the app, provider, label and last use. The signing secret is never returned.
create_payment_webhookCreates 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_webhookDeactivates a webhook. Deliveries to it stop being accepted.
reactivate_payment_webhookTurns a revoked webhook back on with its existing secret.
delete_payment_webhookRemoves 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.

Teams and members

An account holds one or more teams, and apps belong to a team. Creating a team and inviting someone are separate operations.

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

Apps, keys and links

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

Exporting your account

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.

Auth
Bearer, a signed-in user session, plus the apikey header.
Export to a filebash
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.json

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

Migration import

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.

Auth
Bearer session token.
Request and responsejson
// 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.

Auth
Bearer session token. The job must belong to your account and be pending.

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.

Auth
Bearer session token.
200 OKjson
{
  "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
}
StatusMeaning
400Missing fields, unsupported platform, or job not in a pending state.
401Missing or invalid session.
403The application does not belong to your account.
403Code FEATURE_NOT_AVAILABLE: your plan does not include the importer.
404Job not found.
413File over 5 MB or over 50,000 rows.
415Unsupported content type.
500A fault on our side. Safe to retry.

Error codes

Ingestion errors, and the 403 a plan gate returns, carry a machine readable code. Other endpoints return {"error": "..."} with the HTTP status alone.

CodeMeaningWhat to do
VALIDATION_ERRORA field is missing, malformed or out of range.Fix the payload. Retrying unchanged will fail again.
AUTH_ERRORBad signature, unknown account, inactive account, or outside the replay window.Verify the canonical payload rules and check for clock drift.
RATE_LIMITToo many requests.Back off exponentially and retry.
FEATURE_NOT_AVAILABLEYour plan does not include this feature. HTTP 403; the body names the feature.Upgrade the plan. Retrying will not help.
QUOTA_EXCEEDEDThe monthly event allowance is exhausted.Upgrade the plan or wait for the monthly reset. Retrying will not help.
INTERNAL_ERRORSomething failed on our side.Retry with backoff. If it persists, contact support.

Android SDK

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.

Initialisationkotlin
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
    )
)

Methods

init(context, apiKey, config)Unit
Starts the SDK, resolves the API key, and emits a one time install event plus a session start. Calling it twice is a safe no-op. A blank key logs an error and leaves the SDK inactive rather than throwing, so it can never crash Application.onCreate.
trackEvent(eventType, properties)Unit
Queues an event. Accepts a String 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)Unit
Records the user identifier and traits as a custom 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)Unit
Registers a callback for the deferred deep link payload of this install, if the install came from a Measura link with a configured destination. Called at most once, on the main thread - safe to register before or after resolution finishes, since a payload resolved earlier is held and delivered as soon as a listener is registered. Most installs have nothing to deliver and never call it. Pass null 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?
For a Measura link that opens the app while it is already installed. Pass the intent's data from the Activity that receives the link. Records a 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()Unit
Stops and resumes collection. Persisted, so an opt-out survives a restart. Do not reapply it on launch.
setPushToken(token)Unit
Registers this device's FCM token so uninstalls can be detected. Optional: no token, no uninstall detection, nothing else changes. Call it from onNewToken and once at startup.
setIntegrityToken(token)Unit
Forwards a Play Integrity token for server-side verification. The verdict is a fraud-scoring tag; no event is discarded because of it. Build the request with Measura's Cloud project number, exposed as Measura.CLOUD_PROJECT_NUMBER, or the token cannot be decoded.
flush()Unit
Requests an immediate send.
isInitialised()Boolean
Whether init has completed.

Configuration

OptionDefaultEffect
batchSize50Events buffered before a send is triggered.
flushIntervalMs30000Timer driven flush interval.
wifiOnlyModetrueHold events until Wi-Fi is available. See the note below.
suppressOnLowBatterytrueDefer sending on low battery.
lowBatteryThreshold15Battery percentage below which sending is deferred.
maxRetryAttempts5Retries before the batch is written back to disk.
maxOfflineQueueSize10000Persistent queue depth. Oldest events drop first.
ingestEndpointapi.measura.dev/v1/ingestOverride for self hosted or staging. Must be https.
logLevelNONENONE, ERROR, DEBUG or VERBOSE.
collectAdvertisingIdtrueRead the Google advertising ID when the user allows it. Set false to never read it.
customerId, appIdnullOptional. 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:

A link into an installed appkotlin
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.

Offline behaviour and delivery

The SDK is built to survive poor connectivity without losing events. Understanding the deferral rules explains most reports of missing data.

When sending is deferred

In every case below the event is written to persistent storage rather than dropped.

  • No network connectivity.
  • wifiOnlyMode is on and the device is on cellular. This is the default.
  • Battery below the threshold with battery suppression enabled.
  • The API key has not finished resolving.

Storage and limits

BehaviourValue
Persistent storeSQLite database on the device
Queue depth10,000 events
Overflow policyOldest dropped first
Batch size50 events
Flush interval30 seconds
Retry attempts5
BackoffDoubling from 1 second
Compressiongzip

When retries are exhausted the batch returns to persistent storage and is retried in a later session rather than discarded.

Was this page useful?