Consent v2 — Complete Integration Guide
Contents
- Overview
- How it works
- Entry URLs
- Request parameters
- OAuth2 integration
- Return to your site
- Notifications (Native and TMF)
- Lookup API
- Code examples
- Error codes
- Troubleshooting
- Onboarding checklist
- Customization overview
- Custom fields
- Multiple forms and show-rules
- Variants and pre-filled links
- Branding and theming
- Languages
- Subscription and payment options
- Add-ons and packs
- Sign-in providers
- Shared core app and multiple journeys
Overview
Consent v2 is a configurable journey service for your app. You send the customer to one URL. We run the steps your app is set up for, which can include number detection, OTP verification, forms, consent, a subscription or a one-time charge, and payment. Then we return the customer to your site with a reference.
This section is for developers, merchants and vendors who integrate with the service. It covers how to call it, what comes back, and what you can customize.
What a journey can include
| Step | What happens |
|---|---|
| Number detection | We detect the customer's mobile number on mobile data. If that is not possible, the customer types it. |
| OTP | The customer confirms the number with a one-time PIN. |
| Forms and custom fields | You collect the details you need: text, choices, locations, files, scanned codes and more. |
| Sign-in (optional) | The customer can identify themselves with Google, Microsoft or a third-party provider. |
| Consent | The customer reads and accepts the terms you configured. |
| Subscription or charge | A new subscription, or a one-time payment. |
| Payment | Operator billing or another payment method, depending on your setup. |
| Outcome | The customer sees a receipt and returns to your site. |
Steps that your app does not need are skipped. You choose the combination during onboarding.
The integration in three steps
- Send the customer to your entry URL (or start an OAuth2 authorization).
- Receive the redirect back to your site, and the server-to-server notification we POST to your endpoint.
- Confirm the result from your backend with the lookup API before you deliver value.
What you need before you start
| Item | Where it comes from |
|---|---|
A registered application and client_id |
Issued during onboarding. |
| A slug | Agreed during onboarding. It becomes part of your public URL, for example summer-promo. |
| A callback (return) URL | You register it during onboarding. We only redirect to registered URLs. |
| A notification endpoint | Optional but recommended. You choose the format and the authentication. |
| An application on the IdeaBiz API Gateway | Used to get the Bearer token for the lookup API. |
The full list is in the onboarding checklist.
How it works
You send the customer to one URL. We run whatever journey your app is configured for, and return the customer to you with a reference.
your site
│ link
▼
https://consent.ideamart.io/v2/offer/{slug}
│
│ we handle: MSISDN detection, OTP, consent,
│ custom fields, subscription or charge, payment
│
▼
your callback?ref=…&ext_ref=…&status=…
│
├── we POST a notification to your endpoint ← push
└── you call the lookup API with either ref ← pull (authoritative)
Three things to hold onto:
- The redirect parameters are a hint. They tell you the journey finished and how, but they arrive through the browser.
- The notification is a convenience. Delivery is at-least-once, so make your endpoint idempotent.
- The lookup API is the truth. Confirm server-side before you deliver value.
The journey is a list of steps
When a customer starts, we build the list of steps for that journey from your app's configuration. The list is fixed for that customer's session, so a configuration change you make later never disrupts a journey already in progress.
| Step | Included when |
|---|---|
| Security check | A fraud-prevention vendor is assigned to your app. |
| Number detection | Your app needs the customer's number and you did not supply a verified one. |
| Manual number entry | Detection failed, or your app always asks for the number plus an OTP. |
| OTP | OTP is enabled for your app. |
| Phone handoff (QR) | Your app requires mobile data and the customer opened the link on a desktop. |
| Forms and custom fields | Your app collects fields. |
| Sign-in | Your app offers at least one sign-in provider. |
| Consent | Your app has consent configured. |
| Subscription or charge | Your app is configured for one. |
| Payment | The action needs a payment method beyond operator billing. |
| Outcome | Always. |
Steps cannot be skipped or replayed by editing the page. If a customer reaches a step out of order, the journey ends safely (see Error codes, E1005 and E1006).
Features are switched on per app
Each capability is a setting on your app, and everything that could expose a journey to a customer starts switched off. Your journey is only reachable once it has been configured, checked and activated. Until then, your entry URL returns E1002.
Setting precedence
Where a setting can come from more than one place, the most specific one wins:
URL parameter (only where your app allows it)
↓ falls back to
your app's own setting
↓ falls back to
the platform default
This is why you can pass theme_id or banner_id in a link, yet cannot select a banner that has not been approved for your app.
Journeys, slugs and your core app
A journey has its own slug, its own look (theme and banner), and its own consent configuration. One vendor account can run several journeys that share the same underlying registration. See Shared core app and multiple journeys.
Languages
The page language is chosen from, in order of priority: the customer's choice on the page's language switcher, then your lang parameter or the customer's remembered preference, then your app default, then English. See Languages.
Entry URLs
There are five ways in. Pick whichever suits your stack. The journey is identical once started.
| # | Entry | When to use |
|---|---|---|
| 1 | GET /v2/offer/{slug} |
Simplest. One link, SEO-friendly. |
| 2 | GET /v2/offer/{slug}/{variant} |
One link per package, with the pack or amount already fixed. |
| 3 | GET /v2/authorize?... |
You already have an OAuth2 / OIDC client. |
| 4 | GET /web/v1/authorize?client_id=… |
You already integrate with MSISDN Detect. |
| 5 | GET /v2/offer/{slug}/{link_code} (short form /s/{link_code}) |
A pre-filled link you create for one customer: their details already filled in, optionally a subscription, add-ons or a pack code preselected. |
All paths use the /v2/ version prefix on https://consent.ideamart.io, so a future breaking change can ship as /v3/ without moving your existing links.
1. Offer URL
GET https://consent.ideamart.io/v2/offer/{slug}
{slug} is assigned during onboarding, for example summer-promo. The page carries your configured title, description and sharing image, so it can be shared or indexed. No server-side authentication is needed. Your app is identified by the slug.
2. Variant sub-URLs
A variant is a second path segment that arrives with choices already made:
https://consent.ideamart.io/v2/offer/summer-promo base journey
https://consent.ideamart.io/v2/offer/summer-promo/sports-pack SPORTS preselected
https://consent.ideamart.io/v2/offer/summer-promo/premium-bundle BASE + SPORTS + MUSIC
https://consent.ideamart.io/v2/offer/summer-promo/topup-100 one-time charge, LKR 100
A variant can preset the subscription packs, the charge amount, theme, banner, language and custom-field defaults. It can also lock any of them so the customer cannot change what your link chose.
Use a variant rather than ?pack= or ?amount= whenever the value is fixed for a campaign:
| Query string | Variant path | |
|---|---|---|
| Customer edits the URL | Can change the pack or amount | Cannot. The preset is held on our side. |
| Sharing and SEO | Parameters are treated inconsistently | Clean, indexable, own metadata |
| Reporting | Needs parsing to attribute | Attributed directly to the variant |
Query parameters remain the right choice for genuinely dynamic values such as ref, lang, and amount when your app permits a vendor-supplied amount.
Ask for variants to be created during onboarding. They are configured, not passed. See Variants and pre-filled links.
3. OAuth2
See OAuth2 integration.
4. Existing MSISDN Detect link
If you already integrate MSISDN Detect and Consent v2 is switched on for your app, your existing /web/v1/authorize?client_id=… link keeps working. It redirects into the Consent v2 journey automatically. You gain the new steps without changing anything on your side.
If your app runs more than one journey (several slugs sharing one client_id), this automatic handoff goes to whichever journey was set up first. Any additional journey is reached only by its own /v2/offer/{slug} link (entry method 1). Ask during onboarding which journey should own the automatic handoff if that matters to you. See Shared core app and multiple journeys.
5. Pre-filled links
Create one link per customer from your own system. A link carries the customer's details, whether they may edit them, an expiry, how many times it may be submitted, and optionally what to offer. The URL itself holds only an opaque code. See Variants and pre-filled links for the request, the response and the 8-digit access code.
Request parameters
Add these to your offer URL. Anything not listed here is passed through (see the last row).
| Parameter | Required | Description |
|---|---|---|
ref |
Recommended | Your own campaign or session reference. Returned unchanged, and queryable later. |
lang |
No | Language tag, for example si. Overrides the app default. Must be a language enabled for your app. Unknown values are ignored. |
theme_id |
No | Selects one of your assigned themes. Ignored unless the theme allows URL override. |
banner_id |
No | Selects one of your approved banners. Ignored if unknown or unapproved. |
amount |
Conditional | One-time charge amount. Only honoured when your app allows a vendor-supplied amount, and always limited to the minimum and maximum configured for your app. |
pack |
No | Comma-separated subscription pack codes to pre-select, for example pack=SPORTS,MUSIC. Only packs marked as selectable by URL are applied. |
sso |
No | Send the customer straight to a sign-in provider: google, microsoft, or a third-party provider's code such as x12. Ignored unless that provider is approved for your app. The customer can still continue without signing in. See Sign-in providers. |
security_ref |
Conditional | Reference from your anti-fraud vendor, when your app is configured to require one. |
security_vendor |
Conditional | Vendor name, for example evina. |
| (any other) | No | Extra parameters are echoed back to you (see below). |
Example:
https://consent.ideamart.io/v2/offer/summer-promo
?ref=camp-778
&lang=si
&pack=SPORTS
&amount=50.00
Extra parameters
Any other query parameters on your entry URL, such as utm_source, are carried through the journey. You get them back on the return redirect and in the passthrough object of the native notification.
Language behaviour
Your lang tag overrides the app default. It does not override a choice the customer makes on the page. If they use the language switcher, their choice wins for the rest of the journey.
When a customer explicitly picks a language, we remember it against their number for future journeys. Whether your lang tag or that remembered preference takes precedence is a per-app setting, because the right answer differs between campaign traffic and service portals. Ask during onboarding which you want. Your lang tag never overwrites a customer's remembered preference.
More detail is in Languages.
Parameters are checked on our side
Values you pass are only ever applied within what your app permits. An out-of-range amount, a pack that is not selectable by URL, an unapproved banner or an unassigned language does not break the journey. It is ignored, or limited, and the next source applies.
OAuth2 integration
/v2/authorize is a standard authorization-code endpoint. You can use an off-the-shelf OAuth2 or OIDC client library instead of hand-rolling the redirect handling.
Discovery
GET https://consent.ideamart.io/v2/.well-known/openid-configuration
Step 1: authorize
GET https://consent.ideamart.io/v2/authorize
?response_type=code
&client_id={your client_id}
&redirect_uri=https://vendor.com/callback
&scope=msisdn%20consent%20subscription
&state={your CSRF token}
&nonce={your replay guard}
&code_challenge={BASE64URL(SHA256(verifier))}
&code_challenge_method=S256
&ref={your reference}
&variant={variant_slug}
| Parameter | Required | Notes |
|---|---|---|
response_type |
Yes | code only. |
client_id |
Yes | The same client_id as MSISDN Detect. |
redirect_uri |
Yes | Must exactly match one of your registered redirect URLs. |
scope |
Yes | Space separated, from msisdn consent subscription charge fields. |
state |
Recommended | Returned verbatim. We never interpret it. |
nonce |
Recommended | Echoed into id_token. |
code_challenge |
Usually | PKCE is required by default. Send S256. |
code_challenge_method |
Usually | S256. plain is rejected unless explicitly enabled for your app. |
ref |
No | Your own reference, as on the offer URL. |
variant |
No | Apply a variant's presets to an OAuth journey. |
The customer completes the journey, then you receive:
https://vendor.com/callback?code={authorization_code}&state={your state}
On failure you get standard OAuth2 error parameters instead:
https://vendor.com/callback?error=access_denied&error_description=...&state=...
Step 2: exchange the code
POST https://consent.ideamart.io/v2/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code={authorization_code}
&redirect_uri=https://vendor.com/callback
&client_id={your client_id}
&code_verifier={your PKCE verifier}
{
"access_token": "eyJ...",
"token_type": "Bearer",
"expires_in": 900,
"id_token": "eyJ...",
"scope": "msisdn consent subscription",
"ext_ref": "EXT-3f9a1c4e-7b21-4d0e-9c33-5a1f2b8e4d70"
}
Codes are single use with a 120-second lifetime. A second presentation is refused and logged.
Step 3: read the result
GET https://consent.ideamart.io/v2/userinfo
Authorization: Bearer {access_token}
This returns the same body as the native notification, so one parser serves the notification, userinfo and the lookup API.
Two things to be clear about
This is not a login system
The access token is scoped to one journey, not to a user. It expires in 15 minutes and grants read access to that journey's result only. Do not treat id_token as a durable subscriber identity or build a session on it. Use your own session keyed by the MSISDN value we return.
PKCE is on by default
Your client is typically a browser redirect with no secret to protect. Without PKCE, a stolen authorization code is enough to read the journey. If your library does not support PKCE, ask during onboarding rather than assuming plain will be accepted.
OAuth2 errors
The OAuth2 entry uses standard RFC 6749 and RFC 6750 error identifiers, so a client library recognises them. See Error codes.
Return to your site
When the journey ends, the customer is redirected to your registered callback URL:
{your_callback_url}
?ref={your_ref}
&ext_ref={our_reference}
&status={COMPLETED|ABANDONED|FAILED|BLOCKED}
&action_status={CONSENT_RECORDED|SUBSCRIBED|ALREADY_ACTIVE|CHARGED|NONE}
&code={status_code}
| Parameter | Description |
|---|---|
ref |
Your original reference, unchanged. |
ext_ref |
Our journey reference, EXT-{uuid}. Use this for lookup. |
status |
Overall outcome. |
action_status |
What actually happened. |
code |
Status code. See Error codes. |
Any extra parameters you sent on the entry URL are echoed back as well.
status values
status |
Meaning |
|---|---|
COMPLETED |
Journey finished. Check action_status for what was done. |
ABANDONED |
The customer left, or the session expired. |
FAILED |
An action or payment was attempted and failed. |
BLOCKED |
Stopped by security or captcha. |
action_status values
| Value | Meaning |
|---|---|
CONSENT_RECORDED |
Consent captured, nothing else required. |
SUBSCRIBED |
A new subscription was created. |
ALREADY_ACTIVE |
The subscriber was already subscribed. Nothing was billed again. |
CHARGED |
A one-time charge succeeded. |
NONE |
No action configured, or the journey ended before the action. |
The redirect always happens, whatever the outcome. Check status before acting.
What to do for each code
code |
status |
action_status |
Your action |
|---|---|---|---|
S1000 |
COMPLETED |
any | Confirm via lookup, then deliver value. |
S1001 |
COMPLETED |
ALREADY_ACTIVE |
Already a subscriber. Grant access, do not bill again. |
S1002 |
ABANDONED |
NONE |
The customer did not finish. Nothing to grant. |
S1003 |
COMPLETED |
any | Completed, but the anti-fraud check could not be confirmed. Apply your own risk rules. |
E1004 |
ABANDONED |
NONE |
Session expired. Offer the journey again. |
E1006 |
ABANDONED |
NONE |
Out-of-order or replayed access, handled silently. |
E2001 |
BLOCKED |
NONE |
Security vendor refused. Do not grant. |
E2002 |
BLOCKED |
NONE |
The security check could not complete and your policy is to block. |
E2003 |
BLOCKED |
NONE |
Captcha refused. |
E3004 |
ABANDONED |
NONE |
OTP attempts exhausted. |
E5013 |
FAILED |
NONE |
Charge failed. |
E5021 |
FAILED |
NONE |
Payment declined. |
E5022 |
ABANDONED |
NONE |
The customer cancelled at the payment provider. |
E5023 |
FAILED |
NONE |
Payment or QR session expired. |
Warning
The redirect is a hint. Confirm with the lookup API before delivering anything of value.
For OAuth2 entries the redirect carries code and state instead, and failures arrive as standard OAuth2 error parameters. See OAuth2 integration.
Notifications (Native and TMF)
When the journey reaches a terminal state, we POST the collected data to the endpoint(s) configured for your app. Two formats are available, native and TMF. You can register both if you want to migrate between them.
| Property | Behaviour |
|---|---|
| Method | POST (or PUT if you prefer). |
| Auth | None, Bearer, Basic, or HMAC signature. Your choice at onboarding. |
| Timeout | 10 s default. |
| Retries | 3 by default, with exponential backoff. |
| Delivery | At-least-once. Be idempotent on ext_ref. |
| Expected response | Any 2xx. Anything else is retried. |
| Target URL | Must be HTTPS. |
If delivery fails after all retries, nothing is lost. The journey stays retrievable through the lookup API, which is the authoritative interface.
HMAC verification
If you choose HMAC, we send a hex-encoded SHA-256 HMAC of the raw request body in the header you nominate (default X-Signature):
X-Signature: 4f8a2c... = HMAC_SHA256(shared_secret, raw_body)
Compute it over the raw bytes before any JSON parsing, and compare using a constant-time comparison. See Code examples.
Native notification format
Flat and explicit. Recommended unless you have a reason to want TMF.
{
"ext_ref": "EXT-3f9a1c4e-7b21-4d0e-9c33-5a1f2b8e4d70",
"merchant_ref": "camp-778",
"core_unique_ref": "HE-cc487c73-e628-40c5-88b5-8ff26e93f840",
"slug": "summer-promo",
"status": "COMPLETED",
"action_status": "SUBSCRIBED",
"status_code": "S1000",
"occurred_at": "2026-09-03T14:02:59+05:30",
"subscriber": {
"msisdn": "IB-A-000A-6a537144-e3be-4728-a136-b94d11083bf3",
"msisdn_mask": "9477*****61",
"msisdn_format": "ATEL",
"verified": true,
"verification_method": "OTP",
"source": "DETECTED",
"operator": "Dialog"
},
"session": {
"device_type": "MOBILE_DATA",
"language": "si",
"language_source": "USER_SELECTED",
"duration_ms": 48200,
"security_result": "VALID",
"security_flag": false,
"captcha_result": "OK"
},
"transaction": {
"txn_ref": "EXTTXN-9f1c8b02-4a55-4c19-b2f8-7d3e6a1c9b44",
"type": "SUBSCRIPTION",
"status": "SUCCESS",
"amount": 30.00,
"currency": "LKR",
"payment_method": "DCB",
"payment_provider": "DCB",
"provider_txn_ref": "DCB-556677",
"subscription_ref": "SUB-88213",
"packs": [
{ "pack_code": "BASE_MONTHLY", "name": "Premium News Alerts", "price": 30.00, "billing_period": "MONTHLY", "optional": false },
{ "pack_code": "SPORTS", "name": "Weekly sports digest", "price": 0.00, "billing_period": "MONTHLY", "optional": true }
]
},
"consent": {
"type": "SUBSCRIPTION",
"accepted": true,
"accepted_at": "2026-09-03T14:02:44+05:30",
"message_shown": "Subscribe to Premium News Alerts for LKR 30.00 / month. You can cancel any time.",
"parameters": [
{ "key": "service_name", "label": "Service", "value": "Premium News Alerts", "mandatory": true, "accepted": true },
{ "key": "price", "label": "Price", "value": "LKR 30.00 / month", "mandatory": true, "accepted": true },
{ "key": "sports", "label": "Weekly sports digest", "value": "", "mandatory": false, "accepted": false }
]
},
"customer": {
"id": 41,
"name": "Nimal Perera",
"email": "[email protected]",
"email_verified": true,
"provider": "google"
},
"addons": [
{ "code": "MUSIC", "name": "Music", "kind": "RECURRING", "billing_period": "MONTHLY", "price": 50.0, "currency": "LKR", "ideabiz_service_id": "h1" }
],
"pack_code": "JUNE50",
"custom_fields": [
{ "key": "nic", "label": "NIC number", "type": "TEXT", "value": "199012345678" },
{ "key": "district", "label": "District", "type": "DROPDOWN", "value": "COLOMBO" },
{ "key": "interests", "label": "Interests", "type": "CHECKBOX", "values": ["SPORTS", "MUSIC"] }
],
"link_events": [
{ "key": "tnc", "url": "https://vendor.com/terms", "clicked_at": "2026-09-03T14:02:31+05:30" }
],
"passthrough": { "utm_source": "facebook", "utm_campaign": "summer" }
}
Notes:
subscriber.msisdnis a tokenised or formatted value according to the number format chosen for your app, not a raw number.msisdn_maskis safe to display.- Single-value custom fields use
value. Multi-value fields usevaluesas an array. custom_fieldsandlink_eventsare omitted if you turn them off in your configuration.customeris present only when the customer signed in with Google, Microsoft or a third-party provider.nameis always filled (the customer is asked for it if the provider did not return one).email_verifiedistrueonly when the provider said so.providerisgoogle,microsoft, or the third-party provider's code. See Sign-in providers.addonslists the add-ons the customer ticked, andpack_codethe pack code they used. Add-ons are recorded only: nothing has been subscribed or charged for them. Each carries its ownideabiz_service_id. See Add-ons and packs.- A location field's
valueis a JSON string{"address","lat","lng"}. A barcode field'svalueis the text inside the code. Photo, PDF and camera fields appear as1 fileor2 files. The files themselves are never sent in a notification. They stay encrypted on the platform and are available to your app's owners in your account portal. customer,addonsandpack_codeare in the native format only. The TMF format does not carry them yet.passthroughcarries any extra query parameters from your entry URL.
TMF notification format
TMF681-flavoured, consistent with the TMF lookup of MSISDN Detect. Journey data that has no natural TMF home is carried in characteristic arrays rather than invented top-level fields.
{
"id": "EXT-3f9a1c4e-7b21-4d0e-9c33-5a1f2b8e4d70",
"href": "/v2/api/tmf/status?ref=EXT-3f9a1c4e-7b21-4d0e-9c33-5a1f2b8e4d70",
"@type": "IndividualIdentification",
"identificationId": "8a395ff9-c181-4c7a-a786-557ab74dd846",
"lifecycleStatus": "Completed",
"validFor": { "startDateTime": "2026-09-03T14:02:11+05:30" },
"verificationResult": true,
"externalIdentifier": [
{ "@type": "ExternalIdentifier", "owner": "merchant", "externalIdentifierType": "merchantRef", "id": "camp-778" },
{ "@type": "ExternalIdentifier", "owner": "ideamart", "externalIdentifierType": "coreRef", "id": "HE-cc487c73-e628-40c5-88b5-8ff26e93f840" }
],
"identifiedPerson": {
"@type": "Individual",
"contactMedium": [
{
"mediumType": "mobile",
"preferred": true,
"characteristic": {
"phoneNumber": "IB-A-000A-6a537144-e3be-4728-a136-b94d11083bf3",
"phoneNumberMask": "9477*****61",
"phoneNumberFormat": "ATEL",
"operator": "Dialog"
}
}
]
},
"characteristic": [
{ "name": "status", "valueType": "string", "value": "COMPLETED" },
{ "name": "actionStatus", "valueType": "string", "value": "SUBSCRIBED" },
{ "name": "statusCode", "valueType": "string", "value": "S1000" },
{ "name": "msisdnVerified", "valueType": "boolean", "value": true },
{ "name": "verificationMethod","valueType": "string", "value": "OTP" },
{ "name": "deviceType", "valueType": "string", "value": "MOBILE_DATA" },
{ "name": "language", "valueType": "string", "value": "si" },
{ "name": "securityResult", "valueType": "string", "value": "VALID" },
{ "name": "transactionRef", "valueType": "string", "value": "EXTTXN-9f1c8b02-4a55-4c19-b2f8-7d3e6a1c9b44" },
{ "name": "transactionType", "valueType": "string", "value": "SUBSCRIPTION" },
{ "name": "amount", "valueType": "number", "value": 30.00 },
{ "name": "currency", "valueType": "string", "value": "LKR" },
{ "name": "paymentMethod", "valueType": "string", "value": "DCB" },
{ "name": "subscriptionRef", "valueType": "string", "value": "SUB-88213" }
],
"consent": {
"@type": "Consent",
"status": "Accepted",
"acceptedDate": "2026-09-03T14:02:44+05:30",
"description": "Subscribe to Premium News Alerts for LKR 30.00 / month. You can cancel any time.",
"characteristic": [
{ "name": "service_name", "value": "Premium News Alerts", "mandatory": true, "accepted": true },
{ "name": "price", "value": "LKR 30.00 / month", "mandatory": true, "accepted": true },
{ "name": "sports", "value": "", "mandatory": false, "accepted": false }
]
},
"relatedParty": [
{ "@type": "RelatedParty", "role": "subscriptionPack", "id": "BASE_MONTHLY", "name": "Premium News Alerts" },
{ "@type": "RelatedParty", "role": "subscriptionPack", "id": "SPORTS", "name": "Weekly sports digest" }
],
"attachment": [
{
"@type": "Attachment",
"name": "customFields",
"mimeType": "application/json",
"content": "{\"nic\":\"199012345678\",\"district\":\"COLOMBO\",\"interests\":[\"SPORTS\",\"MUSIC\"]}"
},
{
"@type": "Attachment",
"name": "linkEvents",
"mimeType": "application/json",
"content": "[{\"key\":\"tnc\",\"clickedAt\":\"2026-09-03T14:02:31+05:30\"}]"
}
]
}
Custom fields and link events sit in attachment with JSON-encoded content, because TMF has no native shape for arbitrary merchant-defined key/value capture. If you find that awkward, use the native format. It is the better fit for this data.
Lookup API
The authoritative check. Query by your reference or ours, in either the native or the TMF format.
Authentication
Use an OAuth2 Bearer token from the IdeaBiz API Gateway.
curl -X POST "https://ideabiz.lk/apicall/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials&client_id=KEY&client_secret=SECRET"
Create a gateway subscription for the lookup APIs during onboarding.
Endpoints
| Method | Path | Query | Format |
|---|---|---|---|
GET |
/v2/api/status |
ref={ext_ref} or txnRef={our_txn_ref} |
Native |
GET |
/v2/api/lookup |
merchant_ref={your_ref} or providerRef={gateway_ref} |
Native |
GET |
/v2/api/tmf/status |
ref={ext_ref} or txnRef={our_txn_ref} |
TMF |
GET |
/v2/api/tmf/lookup |
merchant_ref={your_ref} or providerRef={gateway_ref} |
TMF |
A status call takes exactly one unique selector: ref (our EXT-{uuid}) or txnRef (our EXTTXN-{uuid}). A lookup call takes one non-unique selector: merchant_ref (your own ref) or providerRef (the payment provider's transaction reference, provider_txn_ref). It returns an array. Supplying two selectors at once is rejected with E9002.
Published through the gateway as:
https://ideabiz.lk/apicall/consent-ext/v1/status?ref={ext_ref}
https://ideabiz.lk/apicall/consent-ext/v1/status?txnRef={our_txn_ref}
https://ideabiz.lk/apicall/consent-ext/v1/lookup?merchant_ref={your_ref}
https://ideabiz.lk/apicall/consent-ext/v1/lookup?providerRef={gateway_ref}
https://ideabiz.lk/apicall/consent-ext-tmf/v1/status?ref={ext_ref}
https://ideabiz.lk/apicall/consent-ext-tmf/v1/lookup?merchant_ref={your_ref}
By our reference
GET /v2/api/status?ref=EXT-3f9a1c4e-7b21-4d0e-9c33-5a1f2b8e4d70
Authorization: Bearer {access_token}
Returns the same body as the native notification, so one parser serves both push and pull.
By our transaction reference
GET /v2/api/status?txnRef=EXTTXN-9f1c8b02-...
Same body as ?ref=. txnRef is unique, so this resolves to a single journey.
By your reference or the provider reference
GET /v2/api/lookup?merchant_ref=camp-778
GET /v2/api/lookup?providerRef=PG-55de21aa
Returns an array, because neither your reference nor the provider reference is guaranteed unique on our side:
{
"status": "SUCCESS",
"statusCode": "S1000",
"reference": "camp-778",
"count": 1,
"items": [
{
"ext_ref": "EXT-3f9a1c4e-...",
"merchant_ref": "camp-778",
"status": "COMPLETED",
"action_status": "SUBSCRIBED",
"created_at": "2026-09-03T14:02:11+05:30",
"txn_ref": "EXTTXN-9f1c8b02-...",
"provider_txn_ref": "PG-55de21aa"
}
]
}
Use lookup when the customer never came back (a closed tab, a lost redirect) and you only have your own reference. Use it too when reconciling against a payment provider's report and you only have the provider's providerRef.
Results are scoped to your application. You cannot read another merchant's journeys.
Errors
Failures return { "status": "FAILED", "statusCode": "<code>", "message": "..." }. The TMF endpoints use ERR- codes past authentication. See Error codes.
Code examples
Each example covers the same three parts: send the customer, receive the notification, and confirm server-side. Replace the placeholder helper functions (alreadyProcessed, grantAccess and so on) with your own logic.
PHP
Send the customer:
<?php
// 1. Send the customer
$slug = 'summer-promo';
$ref = session_id();
header('Location: https://consent.ideamart.io/v2/offer/' . $slug . '?' . http_build_query([
'ref' => $ref,
'lang' => 'si',
]));
exit;
Receive the notification:
<?php
// 2. Receive the notification
$raw = file_get_contents('php://input');
$sig = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
$mine = hash_hmac('sha256', $raw, getenv('EXT_HMAC_SECRET'));
if (!hash_equals($mine, $sig)) {
http_response_code(401);
exit;
}
$n = json_decode($raw, true);
// At-least-once delivery: be idempotent
if (alreadyProcessed($n['ext_ref'])) {
http_response_code(200);
exit;
}
if ($n['status'] === 'COMPLETED' && $n['action_status'] === 'SUBSCRIBED') {
grantAccess($n['subscriber']['msisdn'], $n['transaction']['subscription_ref']);
}
markProcessed($n['ext_ref']);
http_response_code(200);
Confirm server-side (authoritative):
<?php
// 3. Confirm server-side
$ch = curl_init('https://ideabiz.lk/apicall/consent-ext/v1/status?ref=' . urlencode($extRef));
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$res = json_decode(curl_exec($ch), true);
curl_close($ch);
if (($res['status'] ?? '') === 'COMPLETED') {
// safe to deliver value
}
Node.js
import express from 'express';
import crypto from 'crypto';
const app = express();
// Raw body is required for HMAC verification
app.post('/api/he-notify',
express.raw({ type: 'application/json' }),
async (req, res) => {
const expected = crypto
.createHmac('sha256', process.env.EXT_HMAC_SECRET)
.update(req.body)
.digest('hex');
const received = req.get('X-Signature') || '';
const a = Buffer.from(expected);
const b = Buffer.from(received);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.sendStatus(401);
}
const n = JSON.parse(req.body.toString());
if (await alreadyProcessed(n.ext_ref)) return res.sendStatus(200);
if (n.status === 'COMPLETED') {
switch (n.action_status) {
case 'SUBSCRIBED':
case 'ALREADY_ACTIVE':
await grantAccess(n.subscriber.msisdn);
break;
case 'CHARGED':
await recordPayment(n.transaction.txn_ref, n.transaction.amount);
break;
case 'CONSENT_RECORDED':
await recordConsent(n.consent);
break;
}
}
await markProcessed(n.ext_ref);
res.sendStatus(200);
});
Python
import hmac, hashlib, requests
def verify(raw: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(secret.encode(), raw, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature or "")
def confirm(ext_ref: str, token: str) -> dict:
r = requests.get(
"https://ideabiz.lk/apicall/consent-ext/v1/status",
params={"ref": ext_ref},
headers={"Authorization": f"Bearer {token}"},
timeout=15,
)
return r.json()
Tips
- Verify the signature over the raw body, before parsing.
- Make the handler idempotent on
ext_ref. - Return a
2xxquickly and do heavy work afterwards, to avoid timeouts and retries. - Never deliver value on the redirect alone. Confirm with the lookup API.
Error codes
The status and error codes you or your customers can see in the redirect, the notification, the lookup API and the journey pages. Each family maps to one area, so a code alone tells you where to look.
| Family | Area | Seen by |
|---|---|---|
S1xxx |
Success and terminal outcomes | You (redirect, notification, lookup) |
E1xxx |
Entry and session lifecycle | Customer (page), you (redirect) |
E2xxx |
Security vendor and captcha | Customer, you |
E3xxx |
OTP | Customer |
E4xxx |
Consent | Customer |
E5xxx |
Subscription, charge and payment | Customer, you |
E6xxx |
Custom fields and trackable links | Customer |
| OAuth2 identifiers | OAuth2 entry (invalid_grant, access_denied, ...) |
Your OAuth client |
E9xxx |
Lookup API and pre-filled link API | Your backend |
Success and terminal status
status values
status |
Meaning |
|---|---|
COMPLETED |
Journey finished. Check action_status for what was done. |
ABANDONED |
The customer left, or the session expired before a terminal step. |
FAILED |
An action or payment was attempted and failed. |
BLOCKED |
Stopped by security or captcha. |
action_status values
| Value | Meaning |
|---|---|
CONSENT_RECORDED |
Consent captured, nothing else required. |
SUBSCRIBED |
New subscription created. |
ALREADY_ACTIVE |
The subscriber was already subscribed. No new request was made. |
CHARGED |
One-time charge succeeded. |
NONE |
No action configured, or the journey ended before the action. |
Success codes
| Code | HTTP | status |
Meaning |
|---|---|---|---|
S1000 |
200 | COMPLETED |
Journey completed successfully. |
S1001 |
200 | COMPLETED |
Completed with nothing to do, already subscribed. |
S1002 |
200 | ABANDONED |
The customer did not finish. Recorded, no action taken. |
S1003 |
200 | COMPLETED |
Completed but flagged. A security validation error was allowed through by your app's policy. |
S1003 is a genuine success for the customer and a signal for you. The journey completed, but the anti-fraud check could not be confirmed. Treat it as lower confidence than S1000 if your risk model cares.
E1xxx: entry and session
| Code | HTTP | Condition | Customer sees |
|---|---|---|---|
E1001 |
404 | Slug not found. | Branded not-found page. |
E1002 |
403 | Journey inactive or not enabled. | "This offer is not available". |
E1003 |
400 | Session not found. | "This link is not valid". |
E1004 |
410 | Session expired. | Expired page with a restart link. |
E1005 |
400 | Step accessed out of order, or replayed. | "This link has already been used". |
E1006 |
302 | Same as E1005, handled silently. |
Redirected to you with status=ABANDONED, session flagged. |
E1007 |
409 | A desktop tab tried to advance after the phone took over. | "Continue on your phone". |
E1008 |
400 | A required parameter is missing or malformed on entry. | Branded error page. |
E1011 |
409 / 410 | A pre-filled link with a submit limit has been used that many times. 410 on entry, 409 if a journey already open on it is submitted. | "This link has already been used". |
Reloading the current step, or reloading the outcome page after completion, is allowed and is not an error. Going back to a completed step gives E1005 or E1006.
E2xxx: security and captcha
| Code | HTTP | Condition | Result |
|---|---|---|---|
E2001 |
403 | The security vendor returned a clear failure. | Always blocked. |
E2002 |
403 | The vendor errored, timed out or was ambiguous, and your policy is to block. | Blocked, flagged. |
E2003 |
403 | Captcha verification failed. | Blocked, flagged. |
E2004 |
403 | Captcha could not be verified and your policy is to block. | Blocked, flagged. |
E2005 |
400 | A security_ref was required but not supplied. |
Treated as a failure, blocked. |
E2010 |
410 | QR pairing token expired. | Expired page, new QR offered. |
E2011 |
409 | QR pairing token already used by another device. | Blocked. |
How your error policy applies
Your app has a policy for security checks that cannot be confirmed (CONTINUE or BLOCK). It governs only the ambiguous case. An explicit vendor rejection always blocks.
| Vendor outcome | Policy CONTINUE |
Policy BLOCK |
|---|---|---|
| Clear success | Proceed. | Proceed. |
| Clear failure | Blocked, E2001. |
Blocked, E2001. |
| Error / timeout / ambiguous | Proceed, flagged, S1003. |
Blocked, E2002. |
E3xxx: OTP
| Code | HTTP | Condition | Customer sees |
|---|---|---|---|
E3001 |
400 | Invalid number format or unsupported operator prefix. | Inline field message. |
E3002 |
200 | The OTP could not be sent. | "Please try again later" with a Resend button. |
E3003 |
410 | Code expired. | "That code has expired" and a request for a new one. |
E3004 |
429 | Verify attempts exhausted. | "Too many attempts". The journey ends. |
E3005 |
429 | Resend requested too soon. | Countdown shown, button disabled. |
E3006 |
429 | Resend limit reached. | Resend hidden. |
E3007 |
400 | Wrong code, attempts remaining. | Inline message and attempt counter. |
E3008 |
403 | The customer tried to change the number, which your app does not allow. | Change option not offered. |
E3004 is terminal for the session. The customer must start a new journey. This is deliberate, since a lockout that could be reset by reloading is not a lockout.
E4xxx: consent
| Code | HTTP | Condition | Customer sees |
|---|---|---|---|
E4001 |
400 | Consent not accepted but required to continue. | "You need to accept before continuing". |
E4002 |
400 | A mandatory item was submitted as unticked. | Mandatory items render locked, so this indicates tampering. |
E4003 |
400 | Unknown consent item submitted. | Rejected, not stored. |
Mandatory items are enforced on our servers, and unknown items are rejected rather than passed into your notification. The record of what a customer agreed to is immutable. Later configuration edits never rewrite it.
E5xxx: subscription, charge and payment
Subscription
| Code | HTTP | Condition |
|---|---|---|
E5001 |
502 | The subscription platform rejected the request or was unreachable. |
E5002 |
400 | The requested pack code is unknown, inactive, or not selectable by URL. |
E5003 |
409 | No default pack configured for this journey. |
E5004 |
502 | The subscription status check failed and could not be assumed safe. |
An already-active subscriber is not an error. It returns S1001 with action_status=ALREADY_ACTIVE.
Charge
| Code | HTTP | Condition |
|---|---|---|
E5010 |
400 | Amount outside the minimum and maximum configured for your app. |
E5011 |
403 | You supplied an amount, but vendor-supplied amounts are not allowed. |
E5012 |
400 | No amount available: none supplied and no default configured. |
E5013 |
502 | The charging service rejected the request or was unreachable. |
Limits are enforced server-side even when you supply the amount, so a tampered URL cannot raise the charge.
Payment
| Code | HTTP | Condition |
|---|---|---|
E5020 |
400 | No payment method available for this device and configuration. |
E5021 |
402 | The provider declined the payment. |
E5022 |
499 | The customer cancelled at the provider. |
E5023 |
410 | The payment or QR session expired at the provider. |
E5024 |
502 | The provider was unreachable, or the response could not be verified. |
E5025 |
400 | The provider's callback signature was invalid. The response is not trusted. |
E5026 |
409 | A recurring subscription needed a stored card token and none was returned. |
E5027 |
403 | The selected saved card is not eligible for reuse. |
E5028 |
400 | The selected payment provider is not enabled for this journey. |
A saved card is only offered when the current session is OTP-verified and the card was itself stored under OTP verification. The browser return from a provider is never trusted on its own. We confirm the outcome with the provider server-side.
E6xxx: custom fields and links
| Code | HTTP | Condition | Customer sees |
|---|---|---|---|
E6001 |
400 | Required field missing. | Inline "this is required". |
E6002 |
400 | Value did not match the field's pattern. | The field's own message. |
E6003 |
400 | Value outside the minimum or maximum length. | Inline length message. |
E6004 |
400 | Submitted option is not one of the configured options. | Rejected, not stored. |
E6005 |
400 | A required link was never opened. | Continue stays disabled with an explanation. |
E6006 |
400 | Multiple values submitted for a single-value field. | Rejected. |
E6007 |
400 | Unknown field submitted. | Rejected, not stored. |
E6008 |
400 | Number outside the allowed range. | Inline range message. |
E6010 |
400 | File larger than the field's size limit (hard cap 10 MB). | "The file is too large". |
E6011 |
400 | Not a readable image, or a PDF with unsafe content. | Type-specific message. |
E6012 |
400 | Image too small (shortest side under 320 px). | "Too small or blurry". |
E6013 |
400 | A capture guide needs two photos and only one was sent. | "Please add every photo: Front, Back". |
E6014 |
400 | File format not allowed for the field. | Allowed formats listed. |
E6015 |
400 | The file could not be stored. | "Please try again". |
E6016 |
400 | A pre-filled form was submitted without the "I have checked these details" tick. | "Please confirm the details". |
E6017 |
400 | An optional AI check said the file is not the requested document, on a field set to reject mismatches. | "This doesn't look like the document we asked for". |
E6018 |
400 | A location that must come from the device got a searched or typed place, or the device position was not precise enough. | A message asking the customer to share their device position, or use their phone. |
E6019 |
400 | A scanned code limited to retail types (EAN / UPC) is not a valid code of those types. | "This isn't a valid barcode". |
Validation is enforced server-side. The browser-side check is a convenience only. Because unknown fields and options are rejected, the notification you receive can only contain fields and option values you defined.
OAuth2 errors
The OAuth2 entry uses standard RFC 6749 and RFC 6750 error identifiers, so an off-the-shelf client library recognises them.
Returned on the redirect from /v2/authorize
https://vendor.com/callback?error=invalid_scope&error_description=...&state=...
error |
Condition |
|---|---|
invalid_request |
Missing or malformed response_type, client_id or redirect_uri. |
unauthorized_client |
client_id unknown or inactive, or OAuth is not enabled for the app. |
unsupported_response_type |
Anything other than code. |
invalid_scope |
Requested scope is not allowed for your app. |
access_denied |
The customer abandoned, or the journey was blocked by security or captcha. |
server_error |
Internal failure. |
temporarily_unavailable |
A dependency is unavailable. |
Note
If redirect_uri fails validation, the error is not redirected. An unvalidated redirect target would be an open redirector, so the customer sees a branded error page instead.
Returned from POST /v2/token
{ "error": "invalid_grant", "error_description": "authorization code already used" }
error |
HTTP | Condition |
|---|---|---|
invalid_request |
400 | Missing grant_type, code, redirect_uri, or code_verifier when PKCE is required. |
invalid_grant |
400 | Code unknown, expired, already used, or issued to a different redirect_uri. |
invalid_client |
401 | client_id unknown or mismatched. |
unsupported_grant_type |
400 | Anything other than authorization_code. |
invalid_scope |
400 | Scope escalation attempted at exchange. |
invalid_grant deliberately covers several distinct failures with one identifier, so it does not tell an attacker which one they hit.
Returned from GET /v2/userinfo
Per RFC 6750, in the WWW-Authenticate header:
error |
HTTP | Condition |
|---|---|---|
invalid_token |
401 | Token unknown, expired or revoked. |
insufficient_scope |
403 | Token lacks the scope for the requested data. |
Lookup API errors
| Code | HTTP | Condition |
|---|---|---|
E9001 |
400 | No reference supplied (or, for the pre-filled link API, links is empty or not an array). |
E9002 |
400 | More than one selector supplied, or (pre-filled link API) a malformed link, value, number, date, maxUses or offer, or more than 1000 links. The message names which. |
E9003 |
400 | (Pre-filled link API) The body is not a JSON object. |
E9010 |
401 | Missing Bearer token. |
E9011 |
401 | Invalid or expired token. |
E9012 |
403 | (Pre-filled link API) The gateway token cannot be tied to one app, so it cannot create links. |
E9030 |
403 | Application not registered, or the journey belongs to another app. |
E9040 |
404 | No journey found for the given reference. (Pre-filled link API: the slug is not an app of the calling application.) |
E9041 |
409 | (Pre-filled link API) Pre-filled links are not enabled for the app. |
E9050 |
500 | Internal error. |
Body shape: { "status": "SUCCESS|FAILED", "statusCode": "<code>", "message": "..." }.
Results are scoped to the calling application. One merchant cannot read another's journeys.
TMF endpoints
The TMF-format endpoints use TMF-style codes past authentication.
| Code | HTTP | Condition |
|---|---|---|
ERR-0001 |
400 | Missing or invalid required parameter. |
ERR-0004 |
404 | Resource not found. |
ERR-0005 |
500 | Internal error. |
{
"code": "ERR-0004",
"reason": "Resource not found",
"message": "No journey found for the given reference",
"status": "404"
}
Authentication failures happen before the TMF handler runs, so those responses use the E90xx shape rather than TMF codes.
Sign-in has no customer-facing code
A failed or cancelled sign-in returns the customer to the sign-in page with a general message and does not say why. A stale or already-used sign-in link shows a plain "This sign-in link has expired" page (E1003).
Troubleshooting
| Symptom | Likely cause | What to do |
|---|---|---|
| Landing page shows a QR code on desktop | Your app requires mobile data. | Expected. The customer continues on their phone so the number cannot be swapped. |
status=NOT_DETECTED then a number entry screen |
WiFi, or no header enrichment. | Expected. Detection needs mobile data. The journey falls back to manual entry plus OTP. |
lang parameter ignored |
Language not enabled for your app. | Ask for it to be added. Unknown values are ignored by design. |
banner_id ignored |
Banner not approved for your app. | Approval is required before a banner can be selected by URL. |
amount ignored or rejected |
Vendor amounts disabled, or outside the minimum and maximum. | Amounts are always limited server-side. |
| Saved card not offered | OTP disabled, or the card was stored without OTP verification. | Both the session and the card must be OTP-verified. |
| Notification never arrives | Endpoint unreachable, non-2xx response, or retries exhausted. | Ask support to check the delivery log, and use the lookup API meanwhile. |
| Notification arrives twice | At-least-once delivery. | Make your handler idempotent on ext_ref. |
| HMAC check fails | Signature computed over parsed JSON. | Compute it over the raw body bytes before parsing. |
status=BLOCKED |
Security vendor or captcha refused. | A genuine refusal. Support can see the flags on the session. |
| Journey completed but nothing granted | You acted on the redirect only. | Confirm with the lookup API before delivering value. |
| A pre-filled link says "already been used" | The link's submit limit was reached (E1011). |
Create a new link, or ask for the limit to be raised. |
| A pack code is refused | Wrong, expired, used up, not accepted by the app, or more than 10 tries in an hour. | Check the code, its expiry and its usable count in your account portal. |
| A sign-in button is missing | The provider is not yet approved, is switched off, or its client has no secret. | Ask your onboarding contact to approve it. |
| Sign-in "didn't complete" | The customer cancelled, opened the sign-in in another browser, or the provider refused. | Expected. The customer can try again or continue without signing in. |
| A scanned code is refused | Wrong code type, fails the value format, or (EAN / UPC) a bad check digit (E6019). |
Check the accepted types and the pattern on the field. |
| Lookup returns nothing for your ref | Wrong reference, or the journey belongs to a different app. | merchant_ref must match exactly, and results are scoped to your app. |
| Entry URL says "This offer is not available" | The journey is not active yet (E1002). |
Confirm with your onboarding contact that it has been activated. |
| Customer sees "This link is not valid" or an expired page | The session is unknown or has expired (E1003, E1004). |
Start a new journey from your entry URL. |
If you still need help, send your onboarding contact the ext_ref, your ref, the time of the attempt and the code you received. See Error codes for what each code means.
Onboarding checklist
Work through this list with your onboarding contact. Items marked "if" apply only when you use that feature.
- [ ] Core app registered and
client_idissued. - [ ] Consent v2 enabled and your slug agreed. This becomes your public URL.
- [ ] Running several campaigns or brands under one
client_id? Each journey gets its own slug. Say which one (if any) should receive the automatic/web/v1/authorizehandoff. See Shared core app and multiple journeys. - [ ] Callback / return URL registered.
- [ ] Journey agreed: OTP mode, consent type, subscription or charge, payment methods.
- [ ] Notification endpoint registered, and format chosen (native or TMF, or both).
- [ ] Notification authentication chosen. HMAC secret exchanged if applicable.
- [ ] Custom fields defined, with value-format patterns tried in the pattern editor in your account portal.
- [ ] If you collect photos, PDFs or codes: field limits and capture guides agreed, and optional AI checks and an AI budget set for the app.
- [ ] If you use several forms: the show rules written down and tested with each answer.
- [ ] If you use pre-filled links: lock, expiry and submit limit decided, and code entry (needs captcha) agreed.
- [ ] If you offer add-ons or pack codes: IdeaBiz subscription ids for each add-on, and which subscriptions they go with.
- [ ] If you want sign-in: provider(s) chosen, redirect URI
https://consent.ideamart.io/v2/sso/callbackregistered with each OAuth client, and approval requested. - [ ] Trackable links (such as terms) supplied, and whether a click is mandatory.
- [ ] Languages enabled, and language priority decided (your
langtag or the customer's remembered choice). - [ ] Theme and banner artwork supplied, with alt text.
- [ ] Page title, description and sharing image supplied.
- [ ] Analytics IDs supplied, if you want secondary tags.
- [ ] Gateway subscription created for the lookup APIs.
- [ ] Tested end to end on mobile data and on desktop (desktop may show the QR handoff).
Before you go live
- Run a complete journey for each slug and each variant you plan to publish.
- Test failure paths too: abandon halfway, enter a wrong OTP, and cancel a payment.
- Confirm your endpoint handles a repeated notification safely.
- Confirm that your backend checks the lookup API before granting anything.
Customization overview
Consent v2 is configured, not coded. You describe the journey you want, and we run it. Your onboarding contact (or your account portal, where your account has one) applies the settings. Everything on this page is opt-in. An app that turns none of it on runs the basic journey.
What you can customize
| Area | What you can change | Page |
|---|---|---|
| What you collect | Text, choices, locations, files, scanned codes and links, with validation. | Custom fields |
| How you link | Variants with fixed choices, and per-customer pre-filled links. | Variants and pre-filled links |
| How it looks | Colors, logo and banners. | Branding and theming |
| Form layout | Several forms in a row, with rules for when each shows. | Multiple forms and show-rules |
| Language | Which languages, and which source wins. | Languages |
| What you sell | Subscription, one-time charge, payment methods. | Subscription and payment options |
| Extras | Add-ons and pack codes. | Add-ons and packs |
| Identity | Google, Microsoft or third-party sign-in. | Sign-in providers |
| Several brands | Multiple journeys on one registration. | Shared core app and multiple journeys |
How settings are applied
Where a setting can come from more than one place, the most specific wins:
- A URL parameter, only where your app allows it for that setting.
- Your journey's own setting.
- The platform default.
A link can therefore adjust a theme or language, but cannot select anything your app has not been approved to use.
Safe by default
- Everything that could expose a journey to a customer starts switched off. A journey is checked before it goes live.
- Validation always runs on our servers. A customer cannot skip a rule by editing the page.
- Changing your configuration never affects journeys already in progress, and never rewrites what a customer already agreed to.
The onboarding checklist
The onboarding checklist lists what to decide and supply for each of these areas.
Custom fields
Beyond the customer's number and consent, a journey can collect extra details. Fields are configured for your app, shown on the page you choose, validated on our servers, and delivered to you in the custom_fields part of the notification.
Where a field can appear
| Page | Use for |
|---|---|
| Form page | The usual place for details. Supports several forms. |
| Consent page | Details tied to accepting the terms. |
| Number page | Details tied to the mobile number. |
| Payment page | Details tied to payment. |
File, camera and barcode fields can only be placed on the form page or the consent page.
Field types
| Type | What the customer sees | Value you receive |
|---|---|---|
| Text, long text | Text box. | The text. |
| E-mail, URL, phone, date | Typed value with the format checked. | The text. |
| Number | Number with optional minimum and maximum. | The number. |
| Dropdown, radio | One option, from a fixed list or from an API. | The option value. |
| Checkbox, multi-select | Several options. | A list of option values. |
| Location | Map with address search and "detect my location". | {address, lat, lng} as a JSON string. |
| Open a link | A card that opens a page, such as Terms. | AGREED once opened. |
| Photo | Photo upload (JPG, PNG, WebP). | 1 file / 2 files. |
| PDF upload. | 1 file. |
|
| Camera | Live camera capture, with an optional guide. | 1 file / 2 files. |
| Barcode | Scan a QR code or barcode. | The text inside the code. |
Labels, help text and placeholders can be translated per language. If a translation is empty, the label you typed is used. A help text or placeholder with no translation and no typed text is not shown.
Option values are checked against your configured set. A value that is not one of your options is refused, so a notification only contains values you defined.
Value format (patterns)
Text, long text, e-mail, URL, phone and barcode fields can carry a pattern and a message shown when it does not match.
- The whole value must match the pattern.
- A pattern can be at most 255 characters.
- Patterns that can freeze a page on some inputs are refused when you save them.
- Ready-made patterns are offered, for example Sri Lankan NIC, mobile number, passport, vehicle number, postal code, digits, letters, e-mail and https link.
- A try values box lets you type sample answers and see which are accepted, using the same rule customers get. Use it before go-live.
The pattern runs again on our servers when the customer submits.
Open a link (terms and conditions)
A link field shows a card. Opening it is recorded by the server, and then its tick appears. The tick cannot be set by hand.
| Setting | Effect |
|---|---|
| Link address | Must start with https://. |
| Link text | Text on the card (default "Read Terms and Conditions"). |
| Reference parameter | The name of a query parameter that carries this journey's ext_ref to your page. Default ref. |
| Require reading | Continue stays disabled until every required link was opened. We also enforce this on the server. |
You receive each click time in link_events.
Location
A location field stores the address and coordinates. It can be set to device only, so the customer can share only the device's own position (no search, no tapping the map) with an accuracy of 1000 m or better. On a computer, the page offers to finish this on the phone. A pre-filled link cannot carry a device-only location.
This stops typing in an address. It cannot prove a position against someone who fakes their GPS.
Photos, PDFs and camera
- Files must be within the field's size limit (never above 10 MB).
- The file type is decided from its content, not from its name.
- Images must be readable and not too small (shortest side at least 320 px). They are re-saved on our side, which removes hidden metadata such as GPS and device details.
- PDFs with encryption, scripts, launch actions, attachments or rich media are refused.
- Files are stored encrypted and are never sent in a notification. Your app's owners can view them in your account portal.
- A capture guide tells the customer how to take the photo: a frame (oval, card, passport, A4 or none), which camera, tips, and one or two sides (for example a NIC front and back). Standard guides are available. Guides you create for your own app are reviewed before use.
- Use your phone: on a computer, a camera field, a required photo or a device-only location offers a QR code so the customer can finish that part on their phone, then continue on the computer.
Optional AI check
A photo or PDF field can ask for an automatic check that the file is the document you described. It can also read listed details (such as a NIC number) into empty fields that the customer then confirms. Each app has a monthly usage budget. If the check errors, has no budget or is unsure, the customer is never blocked. Only an explicit mismatch on a field set to reject mismatches refuses a file.
Your account portal has a test box on each file field where you can upload a sample and see each check without saving anything.
QR codes and barcodes
A barcode field stores the text inside a QR code or barcode.
| Setting | Meaning |
|---|---|
| Code types accepted | Any of QR, Data Matrix, PDF417, Aztec, EAN-13, EAN-8, UPC-A, UPC-E, Code 128, Code 39, Code 93, ITF, Codabar. None ticked means any. |
| Customer can also type the value | On by default. |
| Value format | The same pattern tool as above. |
Customers can scan live with their camera (in browsers that support it), scan from a photo (works on every phone), or type the value if you allow it. A scanned photo is read in memory and never stored. When every accepted type is EAN or UPC, the check digit is verified too.
Note
A scan is not proof that a code was physically scanned. The browser only fills the text box, and we validate the value on submit.
What the customer sees afterwards
The result page lists what the customer entered, in your form's order and the page's language: option names instead of codes, a location's address, "2 files uploaded", "Opened" for a terms link. Empty answers are skipped, and nothing is shown if the journey failed.
Multiple forms and show-rules
The form page can hold more than one form, shown one after another like a wizard. Later forms can appear only when earlier answers say so.
How forms are arranged
- Form 1 always shows. Its title and description are your normal form title and description.
- Forms 2 to 10 each have a title (also their step name), a description and a show rule.
- A form with no rule always shows. So "form 2 is not related to form 1" is simply a form with no rule.
- You place each field on a form with the field's Form setting. This applies to form-page fields.
Show rules
A rule combines up to 10 conditions on answers from earlier forms. It matches when all of them are true, or when any of them is, as you choose.
| Operator | Meaning |
|---|---|
EQ / NEQ |
Is / is not. Case-insensitive, and matches any value of a multi-value answer. |
IN / NOT_IN |
Is one of / none of a list. |
CONTAINS |
The text contains a value. |
FILLED / EMPTY |
Has / has no answer. |
GT / LT |
More than / less than. Numbers only. A missing number never matches. |
Example: show "Business details" only when the answer to "Account type" is BUSINESS, or "Parent details" when "Age" is less than 18.
Behaviour
- Rules run on our servers against the answers already stored (and any pre-filled values). A customer cannot reach a hidden form by editing the page, because the form to show is always recomputed.
- Each form is validated and saved when the customer presses Continue. Back returns to the previous shown form without checking or uploading anything, and keeps the answers.
- The page shows "Form 2 of 3" and a progress bar. With the step bar on, each shown form is one entry named by its title.
- Required links are checked on the last shown form. The 8-digit code box shows only on form 1.
- If a customer goes back and changes an answer so that a form becomes hidden, the answers and uploaded files on that hidden form are removed. They never reach you.
- If you have no extra forms, everything is one form.
Before go-live
Write the rules down and try every combination of answers that decides whether a form shows.
Variants and pre-filled links
Two ways to give a customer a link that already carries choices.
| Variant | Pre-filled link | |
|---|---|---|
| Made for | A campaign or package, shared by many customers. | One specific customer. |
| URL | /v2/offer/{slug}/{variant} |
/v2/offer/{slug}/{link_code} or /s/{link_code} |
| Holds | Subscription packs, charge amount, theme, banner, language, custom-field defaults. | That customer's details, an expiry, a submit limit, and optionally an offer. |
| Created | During onboarding. | By you, whenever you need one. |
Variants
A variant is a second path segment under your slug:
https://consent.ideamart.io/v2/offer/summer-promo/sports-pack SPORTS preselected
https://consent.ideamart.io/v2/offer/summer-promo/premium-bundle BASE + SPORTS + MUSIC
https://consent.ideamart.io/v2/offer/summer-promo/topup-100 one-time charge, LKR 100
A variant can preset the subscription packs, the charge amount, the theme, the banner, the language and custom-field defaults. Any of these can be locked, so the customer cannot change what your link chose. For OAuth2 journeys, pass variant={variant_slug} to apply the presets.
Prefer a variant over ?pack= or ?amount= when the value is fixed for a campaign, because the preset is held on our side and cannot be edited in the URL. See Entry URL.
Pre-filled links
A pre-filled link opens the normal journey with the form already filled in.
https://consent.ideamart.io/v2/offer/{slug}/{link_code} full link
https://consent.ideamart.io/s/{link_code} short link
https://consent.ideamart.io/v2/offer/{slug}?pf={link_code} also accepted
- The URL carries only an opaque code. The values, the lock setting and anything uploaded are held on our side, never in the query string, because a customer can edit a query string.
- A link code is 7 readable characters (no 0, O, 1, I or L).
- Each link also has an 8-digit access code, unique within your app.
- Extra query parameters on the link (
ref,lang, campaign values) are passed through as usual.
Settings that apply
| Setting | Meaning |
|---|---|
| Allow pre-filled links (app) | When off, links cannot be created and existing ones open an empty form. |
| Lock pre-filled fields by default (app) | The customer cannot edit the values. Anything posted for a locked field is ignored and the stored value is used. |
| Lock (per link) | Overrides the app default. |
| Expiry (per link) | After this date the link opens a normal journey. |
| Can be submitted (per link) | Once, a set number of times, or unlimited. A use is a completed journey from the link. |
| Bound mobile (per link) | The 8-digit code then works only for that number. |
| Offer (per link) | A subscription, add-ons or a pack code to preselect. See Add-ons and packs. |
| Disabled | The link stops working at once. |
A link that has reached its limit shows "This link has already been used" (E1011).
Some fields are never pre-filled, because the customer must do them: opened links, photos, PDFs, camera captures and device-only locations.
When values were filled in for the customer, the form says "This form was filled in for you", and the customer must tick "I have checked these details" before continuing (E6016).
Entering a code by hand
A customer who has a code but not the link has three places to type it:
| Code | Where |
|---|---|
| Link code (7 or 8 characters) | The landing page, Have a link code?. Or open /v2/offer/{slug}?code=1 to go straight to the box. |
| 8-digit access code | The form step, Have an 8-digit code? |
| Pack code | The consent step, Have a pack code?, only when your app accepts pack codes. |
Because an 8-digit code can be guessed by a script, code entry is only offered when the customer's number is verified, captcha is enabled and passes, fewer than 5 wrong codes were tried in the journey, and (for a bound link) the same verified number is used. Code entry cannot be switched on without captcha.
Creating links from your own system
POST https://consent.ideamart.io/v2/api/prefill-links
Authorization: Bearer {gateway token}
Content-Type: application/json
{
"slug": "summer-promo",
"batchName": "June offer",
"lock": true,
"expiresAt": "2026-12-31",
"maxUses": 1,
"offer": { "packCode": "GOLD", "addonCodes": ["MUSIC"] },
"links": [
{ "externalId": "APP-1001", "ref": "ORD-77", "msisdn": "0771234567",
"values": { "full_name": "Nimal Perera", "district": "COLOMBO" },
"offer": { "bundleCode": "JUNE50" } }
]
}
| Field | Notes |
|---|---|
slug |
Your journey. It must belong to the calling application. |
links |
1 to 1000 per call. Each may carry its own values, externalId, ref, msisdn and offer. |
lock, expiresAt, maxUses, batchName |
Apply to every link in the call. lock defaults to your app setting. expiresAt is YYYY-MM-DD or YYYY-MM-DD HH:MM:SS. maxUses is 1 to 100000, or omit for unlimited. |
offer |
packCode (a subscription), addonCodes, bundleCode (a pack code). A per-link offer overrides the call's. |
A successful call returns 201:
{ "status": "SUCCESS", "batchId": 12, "locked": true, "maxUses": 1, "codeEntry": true,
"links": [ { "externalId": "APP-1001", "ref": "ORD-77", "msisdn": "9477***33",
"linkCode": "K7M3QXR", "accessCode": "48210937",
"url": "https://consent.ideamart.io/s/K7M3QXR",
"fullUrl": "https://consent.ideamart.io/v2/offer/summer-promo/K7M3QXR" } ] }
The ref you give on a link comes back to you as merchant_ref. Errors use the E9xxx family. See Error codes.
Creating links in your account portal
Your account portal lets you create links one at a time, enter several by form, or upload a CSV or Excel list and map columns to your fields. You can search, filter by state (new, opened, completed, used up, expired, disabled), download the list with QR images, share a link, and show a clean full-screen QR to a customer. You can also set the QR colors, logo and layout.
Branding and theming
Each journey has its own look. You customize colors and logo through your account portal, or supply the artwork during onboarding.
What you can brand
| Element | Notes |
|---|---|
| Theme | The set of colors and styles for your pages. You can have more than one theme assigned to a journey. |
| Logo and banner | Separate artwork for mobile and desktop. A journey can have several banners. |
| Page title, description and sharing image | Used when your offer URL is shared or indexed. |
| Wording | Text on the pages can be reworded per language. |
| Analytics | Optional analytics IDs for secondary tags. |
Choosing a theme or banner from a link
| Parameter | Behaviour |
|---|---|
theme_id |
Selects one of your assigned themes. Ignored unless the theme allows URL override. |
banner_id |
Selects one of your approved banners. Ignored if unknown or unapproved. |
Without a parameter, your default theme and banner show. A variant can also fix a theme or banner for a campaign, and lock it.
Rules to know
- Banners need approval before they can be selected by URL.
- Colors must be readable. Text and background pairs must meet a 4.5:1 contrast ratio before a theme is accepted. This is an automated accessibility check, not a conformance guarantee. Full accessibility validation needs manual testing with assistive technology and expert review.
- Alt text is needed for your artwork.
- Wording changes apply per app and per language together. You can reword a string in Sinhala and leave English as it is. Customer-facing text can only use the placeholders that string allows.
- Pages work on small screens. They are designed mobile first, down to 320 px wide.
What to send during onboarding
- Logo and banner artwork for mobile and desktop, with alt text.
- Your brand colors, or the theme you want to start from.
- Page title, description and sharing image.
- Any wording changes, per language.
- Analytics IDs, if you want them.
Languages
Three parties have a say in the page language, so the order is explicit.
| Priority | Source | Set by |
|---|---|---|
| 1 | The customer picks from the language switcher on the page. | The customer, at any time. |
| 2 | Your lang tag on the entry URL, or the customer's saved preference for their number. |
Your link, or a previous journey. |
| 3 | Your app's default language. | Onboarding. |
| 4 | English. | Platform default. |
Vendor tag or saved preference
Priority 2 holds two competing sources. A per-app setting, language priority, decides which wins when both are present:
- Your
langtag (the default) suits campaign traffic. A Tamil-language creative should land on a Tamil page, even for a subscriber who once browsed in Sinhala. - The customer's saved preference suits service portals, where the subscriber's own past choice matters more than the link they clicked.
Ask during onboarding which you want.
Rules
- Only languages enabled for your app are accepted. An unknown or unassigned
langvalue is ignored and the next source applies, so a stale link degrades instead of erroring. - Only an explicit customer selection is saved against the customer's number. Your
langtag is a campaign instruction, not a personal preference, so it never overwrites a saved choice. - The number is not known at entry, so a saved preference can only be applied once detection or OTP resolves it. The language is then re-resolved, but only if the customer has not already chosen. We do not switch a page the customer just set.
- You receive the language used and which source won in the notification (
session.language,session.language_source).
Translating your content
Labels, help text and placeholders on your custom fields, and wording on the pages, can be translated per language. If a translation is empty, the text you typed is used. See Custom fields and Branding and theming.
What to send during onboarding
- The languages to enable, and your default.
- Language priority: your
langtag or the customer's remembered choice. - Translations for your fields, consent text and any reworded page text.
Subscription and payment options
A journey can end in a consent record only, a new subscription, or a one-time charge. You agree the combination during onboarding. This page describes the choices.
The action
| Action | What happens | action_status you receive |
|---|---|---|
| Consent only | The customer's consent is recorded. | CONSENT_RECORDED |
| Subscription | The customer subscribes to a pack. | SUBSCRIBED, or ALREADY_ACTIVE |
| One-time charge | The customer pays an amount. | CHARGED |
An already-active subscriber is not an error. They get ALREADY_ACTIVE (code S1001), and nothing is billed again.
Subscription packs
- A subscription journey has one default pack and, optionally, additional packs.
- A pack has a code, a name, a price and a billing period (for example daily, weekly or monthly).
- Optional packs can be ticked by the customer on the subscription page.
- To preselect packs from a link, use
?pack=SPORTS,MUSIC, or a variant. Only packs marked as selectable by URL are applied. An unknown or non-selectable pack givesE5002. - Packs you can offer on top of a subscription are described in Add-ons and packs.
One-time charge amounts
- The amount comes from your configured default, from a variant, or (if you enable it) from your
amountparameter. - Your app has a minimum and a maximum. An amount outside them is refused or limited. This is enforced on our servers, so a tampered URL cannot raise the charge.
amountis ignored if vendor-supplied amounts are not enabled for your app.
Payment methods
| Method | How it works |
|---|---|
| Operator billing (DCB) | Charged to the customer's mobile account. Needs mobile data. |
| Card or bank payment | The customer is redirected to the payment provider and returned to the journey. |
| LankaQR | The customer scans a dynamic QR code with their banking app. |
Which methods are offered depends on the device and on what is enabled for your app.
| Situation | Methods offered |
|---|---|
| Mobile data, only operator billing enabled | Operator billing, with no chooser shown. |
| Mobile data, operator billing and card enabled | A chooser: operator billing, or card / bank. |
| Not on mobile data | Card / bank and QR only. Operator billing needs the operator connection. |
| Recurring subscription paid by card | Card tokenisation is required. |
| Recurring subscription by operator billing | The operator handles renewal. |
We confirm every payment outcome with the provider on the server. The customer's browser return is never trusted on its own, and provider callbacks are handled safely if repeated or if the customer closes the tab.
Saved cards
A saved card is offered for one-tap reuse only when both hold:
- the current session is OTP-verified, and
- the card was itself stored during an OTP-verified journey.
Being verified now is not enough. This prevents a card added by someone who merely typed a number from appearing for the real subscriber later. Saved cards also require OTP to be enabled for your app.
OTP options
Your app can detect the number and verify it with OTP, always ask for the number plus OTP, or skip OTP. You can also decide whether a customer may change the number. OTP has limits on attempts, resends and code lifetime. When the attempts are exhausted the journey ends (E3004), and the customer starts again.
Security checks
You can attach a fraud-prevention vendor and a captcha to your journey. For checks that cannot be confirmed, you choose whether to continue (the journey completes with S1003) or to block. A clear failure from the vendor always blocks. See Error codes.
What to send during onboarding
- Which action: consent only, subscription or charge.
- Packs, prices and billing periods, and which pack is the default.
- Charge amount, minimum and maximum, and whether you will supply
amount. - Payment methods you want enabled.
- OTP mode, and your security vendor and captcha choices.
Add-ons and packs
Two ways to extend a subscription offer: add-ons the customer can tick, and pack codes that preselect a subscription, add-ons, or both.
Add-ons
An add-on is an extra the customer can take with a subscription.
- Add-ons are available to apps registered on IdeaBiz. Ideamart apps do not have add-ons.
- Each add-on is its own IdeaBiz subscription. It has its own IdeaBiz service id, a price, and is either recurring (daily, weekly or monthly) or one-time.
- An add-on is offered with every subscription of your app, or only with selected ones.
- On the subscription page, the customer ticks add-ons under the subscription they chose. An add-on box hides when the customer picks a subscription it does not go with. Anything that does not apply, or does not exist, is dropped on our side.
Add-ons are recorded only
The customer's choice is saved with the journey and reported to you. Nothing is subscribed or charged for an add-on yet. Use the addons list in the native notification to fulfil them yourself. Linking add-ons to the subscription and payment step is planned for later.
Packs and pack codes
A pack is a code that selects a subscription, add-ons, or both.
Note
Not to be confused with a subscription pack, which is the thing the customer subscribes to and what the ?pack= parameter selects. Here, a subscription is that, and a pack is the code that points at one.
| Setting | Notes |
|---|---|
| Code | 2 to 32 letters or digits (also _ and -), upper-case, unique within your app. |
| Subscription | One of your subscriptions, or "keep the customer's choice". |
| Add-ons | Any of your add-ons. |
| Expiry | Optional date. The code stops working at the end of that day. |
| Usable count | Optional. The number of completed journeys that used the code. |
How a pack reaches a journey
- The customer types it. With Customers can enter a pack code switched on for your app, the subscription page shows "Have a pack code?". A good code selects its subscription and add-ons. A wrong, expired or used-up code shows an error. A journey gets 10 tries per hour.
- A pre-filled link carries it. Use
offer.bundleCodewhen you create the link. It is applied once, only while the journey has no choice of its own, with the same checks. - A link preselects directly. A link can also preselect a subscription or add-ons without a pack code.
A pack's subscription is honoured even when the customer is not allowed to change subscription. If the customer then picks a different subscription, the code is dropped. The customer can always change what was preselected.
What you receive
In the native notification:
"addons": [ { "code": "MUSIC", "name": "Music", "kind": "RECURRING", "billing_period": "MONTHLY",
"price": 50.0, "currency": "LKR", "ideabiz_service_id": "h1" } ],
"pack_code": "JUNE50"
The reference on a pre-filled link comes back as merchant_ref.
Set up
- Create your add-ons and packs in your account portal (or supply them during onboarding), with the IdeaBiz subscription id for each add-on and which subscriptions they go with.
- Decide whether customers may enter pack codes themselves.
Sign-in providers
An optional step lets a customer identify themselves with an account, after their mobile number. You then receive the customer's name and e-mail with the result, and your app keeps a customer record.
What the customer sees
- A Sign in page after the number steps, with one button per provider your app offers: "Continue with Google", "Continue with Microsoft", or a third-party provider with its own name and icon.
- It is always optional. "Continue without signing in" skips it. The step only exists for apps with at least one approved provider.
- A name is always required. If the provider returns none, the customer is asked to type it.
- You can skip the choice by adding
?sso=google,?sso=microsoftor a third-party provider's code (for example?sso=x12) to the entry URL. The customer goes straight to that provider, once. If they cancel, the normal choice is shown.
Providers
| Provider | Kind | Notes |
|---|---|---|
| OpenID Connect | Shows the account chooser. | |
| Microsoft | OpenID Connect | Work, school, personal, a specific tenant, or a domain. A fixed tenant is enforced. |
| Third-party | OpenID Connect or plain OAuth 2.0 | Your own endpoints, scopes and field names. |
Each client can be shared (set up once by the platform administrator for any app to use) or your own (your own client id and secret for your app).
Every association needs approval. Your choice is pending until approved. Customers only see approved, enabled providers. Changing your own client's id, secret, tenant, endpoints or field names sends it back to pending, while switching a provider on or off does not. A rejection comes with a reason. Approval is checked again when the customer returns from the provider, so withdrawing it takes effect immediately.
Client secrets are stored encrypted and are never shown again.
Third-party providers
You provide:
- A button name ("Continue with ...") and an icon.
- The type: OpenID Connect (the issuer is required) or plain OAuth 2.0 (the profile is read from a userinfo URL).
- The authorize, token and (optional for OIDC) userinfo URLs, scopes, and whether PKCE is used.
- The claim names for the account id, e-mail and name. A dotted path reaches inside nested replies, such as
data.user.email.
Its code is X plus an id, for example X12. One app can offer several.
All URLs must be plain https addresses on the standard port, without a user name, and must resolve to public addresses. Internal and private addresses are refused.
Redirect URI
Register this authorised redirect URI with every OAuth client:
https://consent.ideamart.io/v2/sso/callback
How a sign-in is made safe
- The customer signs in on the provider's own page. We never see a password.
- Each attempt uses a one-time state, a nonce and PKCE, and is tied to the customer's browser. A sign-in link sent to someone else cannot attach their account to your customer's journey.
- The provider's token is checked for audience, nonce, expiry and issuer.
- An e-mail is treated as verified only when the provider says so.
- Starting is limited to 20 attempts per journey per hour.
If a sign-in fails, the customer returns to the sign-in page with "Sign-in didn't complete. You can try again or continue without signing in." The reason is not shown. A stale link shows "This sign-in link has expired".
Customers
A customer is a person known to one app. Apps never see each other's customers. A customer record holds the verified mobile number (hashed and masked), e-mail and whether it is verified, name, and the account ids of the providers they signed in with.
On sign-in, we find the customer by account id first, then by the journey's verified number, then by a verified e-mail, and otherwise create a new one. The same person can arrive by number one day and by Google the next and remain one customer.
What you receive
The native notification gains a customer object, present only when the customer signed in:
"customer": { "id": 41, "name": "Nimal Perera", "email": "[email protected]",
"email_verified": true, "provider": "google" }
provider is google, microsoft or the third-party provider's code. The TMF format does not carry customer yet.
Set up
- Choose the providers, and for each decide between a shared client and your own.
- If you use your own, register the redirect URI above in the provider's console, and supply the client id and secret.
- Request approval.
- Optionally add
?sso=...to your entry URLs.
Shared core app and multiple journeys
One vendor account can run several branded consent journeys that share one underlying registration. You register once, keep a single client_id, and publish as many journeys as you need.
What this gives you
Each journey has its own:
- Slug and public URL:
https://consent.ideamart.io/v2/offer/{slug} - Look: theme, banner, logo and page wording
- Consent configuration: consent text, fields, forms, subscription or charge, and payment options
All journeys share:
- Your registered app and
client_id - Your registered redirect URLs
- Your lookup API access. Results from every journey are scoped to your application.
Typical uses are separate campaigns, separate brands, or separate services under one company.
Reaching each journey
| Entry | Goes to |
|---|---|
https://consent.ideamart.io/v2/offer/{slug} |
That journey. Use this for every journey. |
https://consent.ideamart.io/v2/offer/{slug}/{variant} |
That journey, with the variant's choices. |
/v2/authorize?client_id=…&variant=… |
Your OAuth2 client's journey. See OAuth2 integration. |
/web/v1/authorize?client_id=… |
The first journey set up for the client_id. |
The automatic MSISDN Detect handoff
If you already integrate MSISDN Detect and Consent v2 is switched on for your app, your existing /web/v1/authorize?client_id=… link redirects into a Consent v2 journey automatically.
With more than one journey on a client_id, that automatic handoff cannot know which one you mean. It goes to whichever journey was set up first. Every other journey is reached only by its own /v2/offer/{slug} link.
If it matters to you which journey should receive the automatic handoff, say so during onboarding.
Telling journeys apart in results
- The
slugfield in the native notification names the journey. - Use your own
refvalue to group journeys by campaign. It is returned unchanged on the redirect, in the notification, and in the lookup API. - Lookup results are scoped to your application, so you can look up a journey with your reference whichever of your slugs it ran on.
Setting it up
- Register your app once and get your
client_id. - For each journey, agree a slug during onboarding, and decide its look, fields, consent and action.
- Say which journey, if any, should own the automatic
/web/v1/authorizehandoff. - Test each slug end to end, on mobile data and on desktop.
See the onboarding checklist.