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.

Time~30 min
StackAny SDK + Node Express
OutcomeLive decision loop

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:

  1. Track a signal from the SDK.
  2. Decide what action to take, by asking Allyvate.
  3. Render the chosen action in-app.
  4. Inform via HashID for the regulator-friendly server-side path.
  5. 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.

Use case in this guide — a customer makes a high-value transaction (signal). Allyvate decides whether to pitch insurance, a credit-line increase, or nothing (decision). The app shows an in-app card (action). The customer sees a transactional WhatsApp confirmation that includes a pitch (Inform). When they click, your backend receives a webhook and logs intent (close loop).

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
      }
    }]
  }'
Verify — open panel.appice.io → Live Stream and watch the event appear within ~5s.

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.

About the 100ms latency target — P95 latency on the regional endpoint should be under 100ms. If you see otherwise, check that you're calling the right region (mismatched region calls are slower because they fall back to global).

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.

Why this matters — the PII never crossed your application boundary. From a regulator's perspective, your backend is touching only a hashed identifier. The audit trail is provably PII-free at the integration layer.

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

  1. Trigger the original signal (a real or simulated transaction).
  2. Watch Live Stream in panel.appice.io — the Transaction Completed event should land within ~5s.
  3. Watch Decisions — the Allyvate decision should appear with the chosen action and the reason.
  4. Watch the in-app message render on your test device.
  5. Watch the Inform delivery in Inform → Activity — status should progress from queueddispatcheddelivered.
  6. Tap the in-app message — your backend should receive the webhook within ~1s.
That's the whole loop. Signal → decision → action → notification → outcome — observable end-to-end.

What's next

Full REST API reference Every endpoint, every parameter, every response shape. SDK reference Per-platform SDK reference docs — pick the platforms you ship. Open Architecture How the Traffic Manager lets you switch downstream providers — including us — in under an hour. Security & compliance SOC 2, ISO 27001, regional data residency, the architecture diagrams behind it.