First decision loop
This is the guide we wrote because it's the one we wished we had. In ~30 minutes you'll wire up the entire loop: a signal lands, Allyvate decides what to do about it, an action surfaces in the app, an Inform notification reaches the user, and a webhook closes the loop back to your backend.
Overview
Most decisioning-platform tutorials stop at "track your first event". This one keeps going — because tracking on its own isn't a decision loop. The full loop has five moves:
- Track a signal from the SDK.
- Decide what action to take, by asking Allyvate.
- Render the chosen action in-app.
- Inform via HashID for the regulator-friendly server-side path.
- Webhook the outcome back to your backend so the next decision is better-informed.
You'll come away with end-to-end working code in your sandbox. Replace the example use case (cross-sell of an insurance product after a high-value transaction) with whatever you're actually building.
Prerequisites
- Sandbox credentials — request access if you don't have them
- An SDK installed on at least one platform (5 min) — pick one of iOS, Android, Web, React Native
- A Node Express backend you can hit from your phone or browser (any other framework works too — examples use Express, but the API is just HTTP)
- ngrok or similar tunnel for testing webhooks locally
Step 1 · Track the signal
Wherever your event originates (client SDK or server-side), track it. We'll use the iOS SDK for the client-side example and curl for the server-side equivalent.
From iOS
Appice.shared.track(
event: "Transaction Completed",
properties: [
"amount": 4500.00,
"currency": "AED",
"merchant": "Acme Stores",
"category": "retail",
"highValue": true // your business logic decides this
]
)
From your backend (server-side ingest)
curl -X POST https://api.gcc.appice.io/v1/events \
-H "Authorization: Bearer $APPICE_API_KEY" \
-H "X-Appice-Workspace: $APPICE_WORKSPACE" \
-H "Content-Type: application/json" \
-d '{
"events": [{
"userId": "user_4711",
"event": "Transaction Completed",
"timestamp": "2026-05-06T10:33:11Z",
"properties": {
"amount": 4500, "currency": "AED",
"merchant": "Acme Stores", "category": "retail",
"highValue": true
}
}]
}'
Step 2 · Ask Allyvate to decide
Now ask the platform what to do for this user, given the recent signal. Make the call from your backend (right after persisting the transaction):
// Express handler
app.post('/api/transactions', async (req, res) => {
// 1. Persist transaction in your core system
await db.transactions.create(req.body);
// 2. Ingest the signal to Appice
await fetch('https://api.gcc.appice.io/v1/events', { /* see step 1 */ });
// 3. Ask Allyvate for the next-best-action
const decision = await fetch('https://api.gcc.appice.io/v1/allyvate/decide', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.APPICE_API_KEY}`,
'X-Appice-Workspace': process.env.APPICE_WORKSPACE,
'Content-Type': 'application/json'
},
body: JSON.stringify({
hashId: req.body.hashId,
context: {
channel: 'app',
surface: 'post-transaction-overlay',
locale: 'en-AE'
},
guardrails: {
excludeCategories: ['promotional'], // bank policy: only transactional surfaces here
maxDailyContacts: 2
}
})
}).then(r => r.json());
// 4. Return your normal response — Allyvate's decision is now logged
res.json({ ok: true, decisionId: decision.decisionId });
});
The response from /v1/allyvate/decide looks like:
{
"decisionId": "dec_8f3a4b1c2e",
"action": {
"type": "in-app-message",
"contentId": "msg_insurance_addon_v2",
"ranking": 0.87,
"reason": "Eligible: cross-sell-insurance, not contacted in 14d, segment: GCC-premium"
},
"alternatives": [
{ "type": "in-app-message", "contentId": "msg_credit_limit_increase_v1", "ranking": 0.62 }
]
}
The SDK on the device automatically polls for eligible messages. With the decision already taken server-side, the SDK gets the answer the moment the user surfaces.
Step 3 · Render in-app
This is the easy part — the SDK does the work. If you enabled auto-render in step 0, the in-app message Allyvate just chose will appear automatically on the next view that's rendered.
// iOS — auto-render is on by default in v5
Appice.shared.inAppMessages.autoRender = true
// Or render manually for full control
Appice.shared.inAppMessages.fetchEligible { messages in
guard let m = messages.first else { return }
Appice.shared.inAppMessages.render(m, in: self)
}
The message can be a card, a modal, an embedded slot, or full-screen. Authoring happens in panel.appice.io; rendering happens via the SDK with no extra integration.
Step 4 · Inform via HashID
Now fire a transactional notification — but without sending PII. Your backend has the user's HashID (returned when you first identified them). Use it to trigger:
await fetch('https://api.gcc.appice.io/v1/inform/trigger', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.APPICE_API_KEY}`,
'X-Appice-Workspace': process.env.APPICE_WORKSPACE,
'Content-Type': 'application/json'
},
body: JSON.stringify({
hashId: req.body.hashId,
templateId: 'tpl_txn_confirm_with_pitch_v3',
channel: 'whatsapp',
variables: {
last4: '4422',
amount: 'د.إ 4,500', // localised AED
merchant: 'Acme Stores'
},
priority: 'transactional'
})
});
Notice what's not in this payload: the user's name, phone number, account number. Just the HashID. The Inform service joins to those PII fields server-side, in the same data residency boundary, and never returns them to your backend.
Step 5 · Webhooks — close the loop
When the user clicks the in-app card or taps the Inform notification, you want to know. Subscribe a webhook:
curl -X POST https://api.gcc.appice.io/v1/webhooks \
-H "Authorization: Bearer $APPICE_API_KEY" \
-H "X-Appice-Workspace: $APPICE_WORKSPACE" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-backend.example/appice/webhook",
"events": ["campaign.clicked", "decision.actioned", "campaign.opened"],
"signingSecret": "REPLACE_WITH_RANDOM_32_BYTES"
}'
In your backend, verify the HMAC and act on the event:
import crypto from 'crypto';
app.post('/appice/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.header('X-Appice-Signature');
const expected = crypto
.createHmac('sha256', process.env.APPICE_WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
if (sig !== expected) return res.status(401).send('bad signature');
const event = JSON.parse(req.body.toString('utf8'));
// event.type === 'campaign.clicked' | 'decision.actioned' | ...
// event.decisionId, event.contentId, event.hashId
recordIntent(event); // your CRM or core update
res.json({ ok: true });
});
For local testing, run ngrok http 3000 and use the ngrok URL as the webhook target while you build.
Step 6 · Verify the loop in the dashboard
- Trigger the original signal (a real or simulated transaction).
- Watch Live Stream in panel.appice.io — the
Transaction Completedevent should land within ~5s. - Watch Decisions — the Allyvate decision should appear with the chosen action and the reason.
- Watch the in-app message render on your test device.
- Watch the Inform delivery in Inform → Activity — status should progress from queued → dispatched → delivered.
- Tap the in-app message — your backend should receive the webhook within ~1s.