Universal Links allow iOS to open your app directly from a URL — no redirect page, no browser flash. Requires configuration in both your app and the server.

Réglages → Mobile : Apple Team ID + Bundle ID + Android package + SHA-256
Réglages → Mobile. La page sert /.well-known/apple-app-site-association et /.well-known/assetlinks.json à partir de ces valeurs.

Step 1 — Configure the Server

# .env
APPLE_TEAM_ID=AB12CD34EF       # 10-char Team ID from developer.apple.com
APPLE_BUNDLE_ID=com.yourco.app  # Must match your Xcode bundle ID exactly

The server will automatically serve the AASA file at /.well-known/apple-app-site-association.

Step 2 — Enable in Xcode

  1. Open your project in Xcode → select the target → Signing & Capabilities
  2. Click + Capability → select Associated Domains
  3. Add your domain with the applinks: prefix:
    applinks:links.yourapp.com

    No https://, no trailing slash, no wildcard (*). Just the hostname.

  4. Build and install on a device (Simulator does not support Universal Links)

Step 3 — Verify the AASA File

# Check the file is accessible and valid JSON
curl https://links.yourapp.com/.well-known/apple-app-site-association | jq .

# Use Apple's AASA validator
open https://branch.io/resources/aasa-validator/
⚠️
HTTPS required — Apple fetches the AASA file over HTTPS. A valid TLS certificate is mandatory. Self-signed certificates will not work in production.

Step 4 — Handle Universal Links in Your App

// SwiftUI
.onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { activity in
    guard let url = activity.webpageURL else { return }
    Task {
        if case .matched(let data) = await DeepLinkSDK.shared.handleOpenURL(url) {
            router.navigate(to: data.customData)
        }
    }
}

App Links allow Android to open your app directly from an HTTP URL, without showing a disambiguation dialog.

Step 1 — Configure the Server

# .env
ANDROID_PACKAGE=com.yourco.app
ANDROID_SHA256=AA:BB:CC:DD:...  # SHA-256 of your signing certificate

# Get your SHA-256 fingerprint:
keytool -list -v -keystore release.jks | grep SHA256

Step 2 — Configure AndroidManifest.xml

<activity android:name=".MainActivity">
  <intent-filter android:autoVerify="true">
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    <data
      android:scheme="https"
      android:host="links.yourapp.com" />
  </intent-filter>
</activity>

Step 3 — Verify the assetlinks.json File

# Check the file
curl https://links.yourapp.com/.well-known/assetlinks.json | jq .

# Use Google's Digital Asset Links validator
open https://developers.google.com/digital-asset-links/tools/generator

Step 4 — Handle App Links in Your App

// Handle both cold start and foreground intent
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    intent.data?.let { handleDeepLink(it) }
}

override fun onNewIntent(intent: Intent) {
    super.onNewIntent(intent)
    intent.data?.let { handleDeepLink(it) }
}

private fun handleDeepLink(uri: Uri) = lifecycleScope.launch {
    val result = DeepLinkSDK.handleOpenUrl(uri)
    if (result is DeepLinkResult.Matched) navigate(result.data)
}

Webhooks (HTTP / Pub/Sub)

DeepLink Server can send real-time HTTP POST notifications or publish to Google Cloud Pub/Sub when events occur. You can configure multiple webhook endpoints per tenant, each with its own URL, authentication, and event filter.

Réglages → Webhooks : ajout d'endpoint, secret, filter d'événements
Réglages → Webhooks. Plusieurs endpoints par tenant, secret HMAC chiffré au repos, filtre d'événements et headers custom.

Endpoint Properties

Each webhook endpoint supports the following fields:

FieldTypeDescription
namestringA friendly label for the endpoint (e.g. "Slack alerts")
urlstringThe HTTPS URL that will receive POST requests
hmac_secretstringShared secret used to sign payloads with HMAC-SHA256
api_keystringStatic key sent in the X-Api-Key header for simple auth
eventsstring[]Array of event types to subscribe to (empty = all events)
activebooleanPause or resume delivery without deleting the endpoint

Routed Events

The following event types can be routed to webhook endpoints:

redirect resolve deferred event.* referral.claim banner.click banner.impression test.ping

Create an Endpoint

# Create a new webhook endpoint
curl -X POST /api/v1/webhooks/endpoints \
  -H "X-Api-Key: your-key" \
  -H "Content-Type: application/json" \
  -d '{
    "name":        "CRM sync",
    "url":         "https://crm.example.com/hooks/deeplink",
    "hmac_secret": "whsec_a1b2c3d4...",
    "api_key":     "sk-crm-xyz",
    "events":      ["redirect", "event.*", "referral.claim"],
    "active":      true
  }'

Manage Endpoints

# List all endpoints
curl /api/v1/webhooks/endpoints -H "X-Api-Key: your-key"

# Update an endpoint (e.g. pause delivery)
curl -X PATCH /api/v1/webhooks/endpoints/ep_abc123 \
  -H "X-Api-Key: your-key" \
  -d '{ "active": false }'

# Delete an endpoint
curl -X DELETE /api/v1/webhooks/endpoints/ep_abc123 \
  -H "X-Api-Key: your-key"

Authentication Headers

Every webhook delivery includes two authentication headers:

  1. X-Deeplink-Signature — HMAC-SHA256 of the raw request body, computed with the endpoint's hmac_secret. The value is prefixed with sha256=.
    X-Deeplink-Signature: sha256=e3b0c44298fc1c14...
  2. X-Api-Key — the static API key configured on the endpoint, sent as-is.
    X-Api-Key: sk-crm-xyz

HMAC Signature Verification

// Node.js verification
const crypto = require('crypto');

function verifyWebhook(rawBody, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');
  const actual = signature.replace('sha256=', '');
  return crypto.timingSafeEqual(
    Buffer.from(expected, 'hex'),
    Buffer.from(actual, 'hex')
  );
}

// Express route handler
app.post('/hooks/deeplink', express.raw({ type: '*/*' }), (req, res) => {
  const sig = req.headers['x-deeplink-signature'];
  if (!verifyWebhook(req.body, sig, process.env.WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }
  const event = JSON.parse(req.body);
  // Process event...
  res.sendStatus(200);
});

Webhook Payload

{
  "event":      "redirect",
  "tenant":     "my-company",
  "timestamp":  "2026-04-11T10:00:00Z",
  "endpoint_id": "ep_abc123",
  "data": {
    "slug":     "summer-sale",
    "platform": "ios",
    "country":  "FR",
    "matched":  false
  }
}

Google Cloud Pub/Sub Delivery

As an alternative to HTTP, you can route events to a Pub/Sub topic for parallel, high-throughput processing.

# Create a Pub/Sub endpoint
curl -X POST /api/v1/webhooks/endpoints \
  -H "X-Api-Key: your-key" \
  -d '{
    "name":    "Analytics pipeline",
    "type":    "pubsub",
    "topic":   "projects/my-gcp-project/topics/deeplink-events",
    "service_account_json": "{ ... }",
    "events":  ["redirect", "resolve", "deferred"],
    "active":  true
  }'
💡
The service account needs the roles/pubsub.publisher role on the target topic. Each event is published as a separate message with event type and tenant ID in Pub/Sub attributes.

Test Ping

Send a test.ping event to a specific endpoint (or all active endpoints) to verify connectivity.

# Send test ping to a specific endpoint
curl -X POST /api/v1/webhooks/test \
  -H "X-Api-Key: your-key" \
  -d '{ "endpoint_id": "ep_abc123" }'

# Send test ping to all active endpoints
curl -X POST /api/v1/webhooks/test \
  -H "X-Api-Key: your-key"

Smart Banner

The Smart Banner shows mobile web visitors an "Open in App" prompt, driving them to install or open your app with the correct deep link context.

Smart banners — liste avec preview mobile
Page Bannières — liste à gauche, preview iPhone à droite. Chaque bannière a un variant (bar, floating, fullscreen, interstitial) et un targeting.

Embed the Script

<!-- Add to <head> on every page of your web site -->
<script
  src="https://links.yourapp.com/api/v1/banner/your-tenant/banner.js"
  data-position="top"
  data-dismiss="session"
></script>

Per-Page Configuration

<!-- On a product page, pass the specific link slug -->
<script
  src="https://links.yourapp.com/api/v1/banner/your-tenant/banner.js"
  data-link-slug="summer-sale"
  data-position="bottom"
  data-dismiss="permanent"
  data-theme="dark"
></script>

JavaScript API

// Control the banner programmatically
window.DeepLinkBanner.show();
window.DeepLinkBanner.hide();
window.DeepLinkBanner.setLink('product-42');

// Listen to events
window.DeepLinkBanner.on('open', () => console.log('User tapped Open in App'));
window.DeepLinkBanner.on('dismiss', () => console.log('Banner dismissed'));

A/B Testing

Use the custom_data field and analytics query engine to implement A/B tests on deep link campaigns.

Page A/B Tests — liste des tests en cours, uplift et significativité
Page A/B Tests. Chaque test pondère ses variantes ; uplift et confiance statistique calculés en continu.

Create Variants

# Variant A — original
curl -X POST /api/v1/links -d '{
  "title": "Summer Sale — A",
  "campaign": "summer-ab",
  "source": "email",
  "custom_data": { "variant": "A", "cta": "Shop Now" },
  "web_url": "https://example.com/sale"
}'

# Variant B — control
curl -X POST /api/v1/links -d '{
  "title": "Summer Sale — B",
  "campaign": "summer-ab",
  "source": "email",
  "custom_data": { "variant": "B", "cta": "Get 20% Off" },
  "web_url": "https://example.com/sale?v=b"
}'

Analyze Results

# Compare installs by campaign source
curl -X POST /api/v1/analytics/query \
  -d '{
    "filters": { "campaign": "summer-ab" },
    "group_by": ["slug", "title"],
    "metrics": ["clicks", "installs", "conversion_rate"],
    "order_by": "conversion_rate",
    "order": "desc"
  }'

Statistical Significance

💡
For statistically significant results, aim for at least 1,000 clicks per variant before drawing conclusions. Use a Z-test or chi-squared test on the conversion rates.

Custom Events

Track meaningful actions in your app and attribute them to the deep link that drove the user.

Page Events — flux des événements custom + filtres
Page Events. Visualisation du flux des événements track() par nom, propriétés et lien attribué.

Common Event Patterns

Purchase

await sdk.trackEvent({
  name: 'purchase',
  linkSlug: data.slug,
  properties: {
    product_id: '42',
    category: 'electronics',
  },
  revenue: 99.99,
  currency: 'EUR',
});

Sign Up

await sdk.trackEvent({
  name: 'signup',
  linkSlug: data.slug,
  properties: {
    method: 'email',
    plan: 'free',
  },
});

Level Complete

await sdk.trackEvent({
  name: 'level_complete',
  linkSlug: data.slug,
  properties: {
    level: '5',
    score: '1420',
    time_s: '142',
  },
});

Subscription

await sdk.trackEvent({
  name: 'subscribe',
  linkSlug: data.slug,
  properties: {
    plan: 'pro',
    billing: 'annual',
  },
  revenue: 119.88,
  currency: 'USD',
});

View Event Funnel

# See the full funnel for a campaign
curl "/api/v1/funnel?days=30&link_slug=summer-sale"

# Query event details
curl -X POST /api/v1/analytics/query -d '{
  "filters": { "campaign": "summer_2025" },
  "group_by": ["date"],
  "metrics": ["clicks", "installs"]
}'

Data Export (GCS / S3)

Export event data to your own data warehouse for long-term storage and custom analytics. DeepLink Server supports BigQuery streaming, Google Cloud Storage batch export, and Amazon S3 batch export.

Réglages → Exports : GCS / S3 batch + BigQuery streaming
Réglages → Data Exports. Configuration GCS bucket, AWS S3, BigQuery streaming. Service-account keys chiffrées au repos.

Supported Destinations

BigQuery real-time

Events are streamed in real-time via the BigQuery insertAll API. Rows appear within seconds of the event occurring.

Google Cloud Storage batch

Events are batched into newline-delimited JSON files and uploaded to a GCS bucket on a configurable schedule (hourly or daily).

Amazon S3 batch

Events are batched into newline-delimited JSON files and uploaded to an S3 bucket on a configurable schedule (hourly or daily).

Configuration

Navigate to Settings > Data warehouse in the dashboard, or use the API:

# Get current export configuration
curl /api/v1/export/config -H "X-Api-Key: your-key"

# Configure BigQuery streaming
curl -X PUT /api/v1/export/config \
  -H "X-Api-Key: your-key" \
  -d '{
    "type":       "bigquery",
    "project_id": "my-gcp-project",
    "dataset":    "deeplink_events",
    "table":      "raw_events",
    "service_account_json": "{ ... }"
  }'

# Configure GCS batch export
curl -X PUT /api/v1/export/config \
  -H "X-Api-Key: your-key" \
  -d '{
    "type":       "gcs",
    "bucket":     "my-deeplink-exports",
    "prefix":     "events/",
    "schedule":   "hourly",
    "service_account_json": "{ ... }"
  }'

# Configure S3 batch export
curl -X PUT /api/v1/export/config \
  -H "X-Api-Key: your-key" \
  -d '{
    "type":              "s3",
    "bucket":            "my-deeplink-exports",
    "prefix":            "events/",
    "region":            "eu-west-1",
    "schedule":          "daily",
    "aws_access_key_id": "AKIA...",
    "aws_secret_access_key": "..."
  }'

Test Connection

# Verify credentials and write permissions
curl -X POST /api/v1/export/test -H "X-Api-Key: your-key"

Manual Export

# Trigger an immediate export run (batch destinations only)
curl -X POST /api/v1/export/run \
  -H "X-Api-Key: your-key" \
  -d '{
    "from": "2026-04-01T00:00:00Z",
    "to":   "2026-04-18T00:00:00Z"
  }'
⚠️
Credentials — Service account JSON and AWS secrets are stored encrypted at rest. They are never returned in API responses after creation; only a masked placeholder is shown.

Settings & Configuration

Manage your account, team access, branding, API keys, and domain configuration from the Settings page in the dashboard.

Page Réglages — vue d'ensemble
Réglages — entrée principale. 25 sous-pages organisées en 8 groupes : Account, Brand, Mobile, Webhooks, SSO, Exports, API, Members.
Réglages → Brand : couleur, logo
Réglages → Brand. Couleur d'accent + logo ; appliqué sur la sidebar et certaines pages publiques (smart banner default style).
Réglages → API Keys
Réglages → API Keys. Clés dlk_* à scope tenant, rotation à la volée, révocation immédiate.
Réglages → Members : gestion des utilisateurs/rôles
Réglages → Members. Gestion des users du tenant, rôles (admin/user), invitations.

Account

Update your profile information, change your password, and enable two-factor authentication (2FA) via TOTP.

  1. Navigate to Settings > Account
  2. Update your display name and email address
  3. To enable 2FA, click Enable two-factor authentication, scan the QR code with your authenticator app, and enter the verification code

SSO / SAML

Connect your identity provider (Okta, Azure AD, or any SAML 2.0 IdP) for single sign-on.

  1. Navigate to Settings > SSO/SAML
  2. Enter your IdP's Metadata URL (e.g. https://your-org.okta.com/app/xxx/sso/saml/metadata)
  3. Copy the following values into your IdP configuration:
    # Entity ID (Audience URI)
    https://links.yourapp.com/auth/saml/metadata
    
    # ACS URL (Reply URL)
    https://links.yourapp.com/auth/saml/acs
  4. Click Test connection to verify the SAML handshake before enforcing SSO

Brand

Customize the visual identity used on the redirect page, transactional emails, and smart banners.

# Update branding via API
curl -X PATCH /api/v1/tenants/current \
  -H "X-Api-Key: your-key" \
  -d '{
    "brand": {
      "logo_url":     "https://cdn.example.com/logo.svg",
      "accent_color": "#4F46E5"
    }
  }'

The logo_url is displayed on the redirect interstitial page and in email templates. The accent_color is used for buttons, links, and the smart banner background.

API Keys

Create, rotate, and revoke API keys for programmatic access.

  1. Navigate to Settings > API Keys
  2. Click Create key and give it a descriptive name (e.g. "CI/CD pipeline")
  3. Copy the key immediately -- it is only shown once
  4. To rotate a key, click Rotate. The old key remains valid for 24 hours to allow a graceful migration
  5. To revoke a key, click Revoke. The key is invalidated immediately

Domains (Custom CNAME)

Use your own domain (e.g. links.yourapp.com) instead of the default subdomain.

  1. Navigate to Settings > Domains
  2. Add your custom domain and create a CNAME record in your DNS provider:
    links.yourapp.com.  CNAME  ingress.deeplink-server.com.
  3. Click Verify to confirm DNS propagation. A TLS certificate is provisioned automatically via Let's Encrypt

Setup détaillé — full walkthrough

  1. 1. Plan check — custom domain requires Pro or Enterprise plan
  2. 2. Add domain — Settings → Domains → "Add custom domain" → enter links.yourapp.com → DeepLink generates a verification token
  3. 3. Two DNS records to create:
    # 1. CNAME for traffic routing
    links.yourapp.com.  CNAME  ingress.deeplink-server.com.
    
    # 2. TXT for ownership verification
    _deeplink-verify.links.yourapp.com.  TXT  "dlk-verify=abc123def456..."
  4. 4. Verify — click "Verify" in dashboard. DeepLink polls DNS every 30s for up to 10 minutes. Status shifts: pending → dns_verified → ssl_issuing → active
  5. 5. SSL provisioning — automatic via Let's Encrypt ACME HTTP-01 challenge. Cert renewed 30 days before expiry. Visible in dashboard with expiry date
  6. 6. AASA / assetlinks.json — automatically served on the new domain at /.well-known/apple-app-site-association and /.well-known/assetlinks.json
⚙️
How it works internally — Express middleware src/services/domain-resolver.js reads req.headers.host, looks up tenants WHERE custom_domain = $1, caches result 60s in LRU (max 500 entries), injects req.tenant_id. Cache miss penalty: 1 DB query. Negative cache TTL: 30s. Manual flush: super-admin POST /api/v1/admin/cache/domain-resolver/flush.
⚠️
Limit — currently 1 custom domain per tenant (column tenants.custom_domain). Multi-domain support via dedicated table = 🔮 planned (STORY-29).

Mobile Apps

Register your mobile apps so the server can generate the correct platform association files.

  1. Navigate to Settings > Mobile Apps
  2. iOS — enter your Apple Team ID and Bundle ID. The server generates the apple-app-site-association (AASA) file automatically at:
    https://links.yourapp.com/.well-known/apple-app-site-association
  3. Android — enter your package name and SHA-256 signing certificate fingerprint. The server generates the assetlinks.json file at:
    https://links.yourapp.com/.well-known/assetlinks.json
💡
See the Universal Links and App Links guides above for detailed setup instructions including Xcode and AndroidManifest configuration.

Fraud Detection

DeepLink Server includes a multi-layer fraud-detection engine. Each click receives a fraud_score (0-100) computed at insertion by src/services/fraud-detector.js, a fraud_flags JSON array listing triggered rules, and a suspicious=true boolean if the score crosses the tenant threshold.

Scoring rules

RuleScoreTrigger
Headless UA+30UA matches HeadlessChrome, PhantomJS, Puppeteer
Datacenter IP+25IP belongs to known datacenter range (GCP/AWS/Azure/DO whitelist, MaxMind ASN)
Velocity burst+20> 10 clicks from same ip_hash in < 60 s
TTD = 0+15time-to-deeplink (click → app_open) = 0 ms (humanly impossible)
Missing referer+10referer empty and platform != ios|android (suspicious direct web)

Thresholds

⚙️
Tenant override — adjust the suspicious threshold via tenant_settings.fraud_threshold_suspicious (default 70). Use lower values for stricter filtering, higher for relaxed.

Click row example

{
  "id": "clk_abc",
  "link_id": "summer-sale",
  "platform": "android",
  "fraud_score": 45,
  "fraud_flags": ["datacenter_ip", "missing_referer"],
  "suspicious": false,
  "ip_hash": "a3f8...c2"
}

IP Deduplication

Clicks from the same IP within a short window are deduplicated. The ip_hash (SHA-256 of the IP) is stored per click — never the raw IP address.

Bot Filtering

The server filters known bot user agents using a regularly-updated list. Filtered clicks are not counted in analytics.

Click Cap Enforcement

Use max_clicks on links to limit exposure on promotional campaigns. Once the cap is hit, the link returns 410 Gone.

Targeting Rules

Use targeting_rules.allow_countries and allow_devices to restrict links to specific audiences and prevent off-target click fraud.

Fingerprint Analysis

The deferred match uses probabilistic fingerprinting (SHA-256 of IP + UA + date). A 2-day window limits replay attacks while handling timezone edge cases.

Audit Log Monitoring

# Look for suspicious patterns: many deletes, rapid link creation
curl "/api/v1/audit?action=delete&limit=50"

# Check for high-volume clicks from single IPs
curl -X POST /api/v1/analytics/query -d '{
  "filters": { "matched": 0 },
  "group_by": ["country", "platform"],
  "metrics": ["clicks", "unique_ips"],
  "order_by": "clicks",
  "order": "desc",
  "limit": 20
}'

CLI Reference — deeplink command-line tool

Node.js CLI to automate common operations from terminal/CI: rotate API keys, batch-create links, export audit, run scheduled reports. Critical for CI/CD (e.g. create a deeplink at every mobile release).

Installation (3 methods)

# Method 1 — npm global (production)
npm install -g @deeplink/cli
deeplink --version

# Method 2 — npx ad-hoc (no install)
npx @deeplink/cli links list

# Method 3 — checkout repo + local link (dev)
git clone https://github.com/deeplink/deeplink-server
cd deeplink-server
npm link cli/   # creates global binary pointing at cli/deeplink.js

Configuration (3 sources, ordered by priority)

  1. CLI flags (max priority) — deeplink --api-url=https://api.deeplink.com --api-key=dlk_xxx --tenant=acme links list
  2. Environment variablesDEEPLINK_API_URL, DEEPLINK_API_KEY, DEEPLINK_TENANT
  3. Config file~/.deeplinkrc.json or ~/.deeplink/config.json:
    { "default": "prod", "profiles": {
      "prod":    { "api_url": "https://api.deeplink.com",    "api_key": "dlk_live_...", "tenant": "acme" },
      "staging": { "api_url": "https://api-stg.deeplink.com", "api_key": "dlk_test_..." }
    }}
    Switch profile: deeplink --profile=staging links list

Auth alternative — interactive login

deeplink login
# → opens browser at https://app.deeplink.com/cli-auth?token=<one-time>
# → user logs in + accepts, server returns a dedicated API key "CLI - <hostname>"
# → CLI persists token in ~/.deeplink/credentials (chmod 600)

Commands

CommandDescriptionExample
links listList tenant's links--limit=50 --search="campaign-q2"
links createCreate a link--url=… --campaign=q2 --tags=facebook,paid
links update <slug>Update a link--campaign=q3 --active=false
links delete <slug>Delete a link (interactive confirm)
banners list/create/update/deleteBanner CRUD
ab-tests list/create/promoteA/B + variant promotionpromote ab_42 --variant=B
tenants list (super-admin)All tenants--export=csv > tenants.csv
users list/inviteTenant users + invitationsinvite alice@acme.com --role=editor
statsKPI summary last 30d--period=7d --format=json
auditTail audit log--filter=action:link.created --since=24h
apikeys list/create/revokeAPI key managementcreate --label="CI prod" --scope=write:links
webhooks list/replay/testDLQ + replayreplay wd_abc123
reports run <template_id>Run + send a scheduled report
onboarding statusOnboarding funnel stats
settings get/setRead/write tenant settingset support_email ops@acme.com
diagnosticSelf-test (auth + connectivity + permissions)

Output formats

CI/CD use case — GitHub Actions

# .github/workflows/release.yml — create deeplink at every mobile release
- name: Create release deeplink
  run: |
    npx @deeplink/cli links create \
      --url="https://app.acme.com/release/${{ github.ref_name }}" \
      --campaign="release-${{ github.ref_name }}" \
      --tags="release,${{ github.ref_name }}" \
      --format=json > deeplink.json
  env:
    DEEPLINK_API_KEY: ${{ secrets.DEEPLINK_API_KEY }}
    DEEPLINK_TENANT: acme

Source: cli/deeplink.js (~600 lines) + cli/lib/ (commands, auth, output formatters).

Settings reference — 25 sous-pages organisées en 8 groupes

Référence complète des Settings après refacto v2 (ex-ADM-MIG). 25 sous-pages réparties en 8 groupes thématiques. Visibilité conditionnée par le rôle (admin ou super-admin). Source : SETTINGS_NAV dans public/js/dashboard.js:8962.

GroupeSous-pageIDRôleContenu
Compte
CompteMon compteprofiletousEmail, nom, mot de passe, 2FA TOTP, langue, fuseau, déconnexion sessions actives.
CompteSSO / SAMLssotousOIDC issuer, SAML metadata URL/Entity ID, ACS callback. Cf. SSO.
Espace
EspaceGénéralgeneraltousTenant slug, contact_email, fuseau horaire, IP allowlist, feature flags AI/ML.
EspaceMarquebrandtousLogo URL, couleur primaire (utilisée sur redirect page, emails, banners).
EspaceUtilisateursmembersadminListe, invitations, rôles (admin / member / viewer), désactivation.
EspaceFacturationbillingtousPlan, prochaine facture, lien Stripe Customer Portal (cf. PROD-001), historique invoices.
EspaceRapports planifiésscheduled-reportstousListe reports cron + Slack/email destination + cadence.
Développeur
DéveloppeurClés APIapikeystousGénération dlk_*, révocation, scopes.
DéveloppeurWebhookswebhookstousEndpoints, HMAC secrets, event filters, deliveries log + replay.
DéveloppeurDomainesdomainstousCustom domain (CNAME → DeepLink), TLS auto via Let's Encrypt (cf. ROAD-G4-02).
DéveloppeurApps mobilesmobiletousApple Team ID + Bundle ID, Android Package + SHA-256. Sert AASA + assetlinks.
DéveloppeurSKAdNetworkskantousConversion value table, postback URLs, MMP receiver config.
DéveloppeurServer-side identifyidentifyadminBackfill identity côté serveur (POST /api/v1/identity).
DéveloppeurAttributionattribution-proadminModèle (last/first/linear/position/time-decay), window (jours), allowed sources.
Intégrations
IntégrationsAnalyticsanalyticstousSegment, Amplitude, Mixpanel API keys + forwarding ON/OFF.
IntégrationsData warehousewarehousetousBigQuery dataset + service account JSON, Snowflake (DEF), GCS export.
IntégrationsAd networksadnetworkstousMeta CAPI, Google Ads, TikTok Events API — OAuth flow + cost sync.
IntégrationsMessageriemessagingtousSlack incoming webhook URL (notifications), Teams (futur), email SMTP override.
IntégrationsPayoutspayoutsadminStripe Connect Express (référents reçoivent les récompenses).
IntégrationsParrainagesreferral-settingsadminReward type/amount, double-sided, fraud threshold, email template.
Conformité
ConformitéRGPD & rétentionprivacytousData retention days (GDPR auto-purge cf. STORY-30), export/delete user data, DPA.
ConformitéJournal d'auditauditadminFiltre + table + export. Cf. audit log investigation.
ConformitéIP allowlistallowlisttousCIDR-aware, applique sur sessions JWT (pas API keys).
ConformitéExportexporttousJob-based export GCS/S3 (clicks, events, links, raw data). Cf. ROAD-G4-03.
Support
SupportRapports de bugsbugsadminListe bugs reportés par les users du tenant + status.
Plateforme (super-admin uniquement)
PlateformeLogs plateformeplatformlogssuper-adminRing buffer logs serveur (in-memory amnésie sur restart, cf. CLAUDE.md multi-instance note).
Danger
DangerZone dangereusedangeradminSuppression tenant, purge données complète (irréversible, double-confirm).

Search bar (⌘K) en haut de la sidebar Settings filtre par nom. Vue d'état (overview) en première position regroupe les KPIs config (badges configuré / partiel / non-configuré pour chaque sous-page).

Onboarding wizard — guide pas-à-pas du nouvel intégrateur

Le wizard accompagne un nouveau tenant de la création (config mobile) jusqu'au premier identify() appelé. Source : src/routes/onboarding.js, table onboarding_progress.

Workflow — 6 étapes

#StepCritère "fait"Vérifiable auto via
1mobile_configApple Team ID + Bundle ID renseignés (iOS) OU Android Package + SHA-256 (Android).POST /api/v1/onboarding/verify → check tenants.apple_team_id non-vide ou android_package non-vide.
2sdk_integrated≥1 click ou event avec sdk_instance_id non-null reçu.Auto-coché à la 1ère requête SDK matchée.
3first_link≥1 ligne dans links pour le tenant.Auto-coché après création via dashboard ou POST /api/v1/links.
4first_click≥1 ligne dans clicks.Auto-coché à la 1ère redirection / resolve.
5first_event≥1 ligne dans events.Auto-coché au 1er POST /events/track.
6identify_check≥1 event avec user_id non-null (= sdk.identify(userId) appelé).Auto-coché. Si fail, cf. identity diagnostic.

API endpoints

UX dashboard

┌─ Wizard d'intégration ─────────────────────────────┐
│ Étape 1/6 ✓ Configuration mobile (Bundle ID + Team)│
│ Étape 2/6 ✓ SDK intégré (clic SDK détecté)         │
│ Étape 3/6 ✓ Premier lien créé                      │
│ Étape 4/6 ✓ Premier clic enregistré                │
│ Étape 5/6 □ Premier event tracké  ← BLOCKER ICI    │
│ Étape 6/6 □ identify() appelé                      │
│                                                    │
│ ▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░░░░░  4/6 (67%)               │
│                                                    │
│ [ Vérifier maintenant ]   [ Documentation SDK → ]  │
└────────────────────────────────────────────────────┘

Troubleshoot par étape

Étape bloquéeCause probableAction
mobile_configSettings → Mobile apps : aucun champ rempliRenseigner soit iOS (Team ID + Bundle ID), soit Android (Package + SHA-256).
sdk_integratedSDK pas appelé / configure() jamais invoquéCf. sdks.html + ajouter configure(serverUrl, tenantSlug) au démarrage app.
first_linkAucun lien crééDashboard → Links → "+ Nouveau lien" OU POST /api/v1/links.
first_clickLien créé mais aucun clicWire handleOpenURL(url) côté SDK + tester via Notes app iOS.
first_eventtrackEvent() jamais appeléAjouter sdk.trackEvent('app_opened') au launch.
identify_checkidentify() manquant après loginHook post-login : sdk.identify(userId). Cf. identity diagnostic.

Identity diagnostic admin

Outil admin pour résoudre les "pourquoi cet utilisateur n'est pas attribué" et autres écarts de chaîne d'identité. Disponible uniquement pour les administrateurs (rôle admin du tenant). Source : src/routes/identity.js.

Cas d'usage

Un utilisateur (Sarah, growth manager d'une boutique) signale : "J'ai cliqué sur la pub Facebook et installé l'app, mais je n'apparais pas comme attribuée à cette campagne dans le dashboard." L'admin doit comprendre où la chaîne click → install → identify s'est cassée.

Diagnostic temps réel

GET /api/v1/identity/diagnostic Auth required + admin — health checks identity tenant-scoped

3 health checks sur le tenant courant + recommandations actionnables :

// Response 200
{
  "verdict": "warn",                              // "ok" | "warn" | "fail"
  "checks": [
    {
      "id": "sdk_instance_id_recent",
      "label": "SDK instance ID seen in clicks (last 1h)",
      "pass": true
    },
    {
      "id": "user_id_recent",
      "label": "user_id seen in events (last 1h)",
      "pass": true
    },
    {
      "id": "user_id_ratio_24h",
      "label": "user_id ratio in events (last 24h)",
      "value": "32%",
      "pass": false
    }
  ],
  "recommendations": [
    "Only 32% of events have a user_id. Ensure identify() is called for all authenticated users."
  ]
}

Verdict

  • ok : tous les checks passent (SDK actif + user_id présent + ratio ≥50%).
  • warn : SDK + user_id OK mais ratio user_id < 50% (events anonymes nombreux).
  • fail : SDK absent OU user_id jamais vu (chaîne d'identité cassée).

Workflow d'investigation pour le cas Sarah

  1. Lancer le diagnostic : curl -H "Authorization: Bearer $JWT" $BASE/api/v1/identity/diagnostic. Si verdict=fail → check SDK + identify() manquant. Si verdict=warn → ratio user_id bas, l'événement de Sarah est probablement anonyme.
  2. Inspecter le clic Facebook de Sarah dans Settings → Identity (page admin) ou directement en DB : SELECT * FROM clicks WHERE link_id = ? AND ip_hash = ? ORDER BY created_at DESC LIMIT 5. Vérifier sdk_instance_id, fingerprint, created_at.
  3. Inspecter l'install via match_attempts ou events table — y a-t-il un event install/app_open avec le même sdk_instance_id ? Si oui → cookie déjà connecté.
  4. Vérifier le merge identity via le graph (table identity_aliases) : le sdk_instance_id de Sarah est-il aliasé à son user_id ? Sinon, identify() n'a pas été appelé après login.
  5. Backfill server-side si nécessaire (récupération d'attribution post-hoc) : POST /api/v1/identity avec {user_id, sdk_instance_id, source:"manual_recovery"}. Cette route audite l'opération.

3 patterns courants

PatternSymptômeDiagnosticSolution
Window expirée Click visible mais pas de match install. Le SDK ne récupère pas l'attribution. SELECT created_at FROM clicks WHERE … → l'écart click→install dépasse match_window_hours (default 24h). Augmenter la window dans Settings → Attribution (ou accepter la perte si CTIT > 24h = fraud-suspicious).
SDK non-initialisé Aucun sdk_instance_id jamais vu. verdict=fail sur check #1. diagnostic.checks[0].pass = false. Code SDK absent ou configure() jamais appelé. Vérifier l'intégration SDK dans l'app. Cf. sdks.html.
identify() jamais appelé Events visibles mais user_id IS NULL. verdict=warn. Ratio user_id < 50%. La chaîne anonyme→identifiée n'est jamais raccrochée. Ajouter sdk.identify(userId) dans le hook post-login/signup. Backfill ancien historique avec POST /api/v1/identity.

Cf. également identity graph (vulgarisé) et chantier IDENT-QUAL livré pour le détail technique.

Audit log — investigation ops

L'audit log enregistre toute mutation administrative (création/modif/suppression de liens, changements de settings, login/logout, etc.). Endpoint admin-only, tenant-scoped pour les admins de tenants, traversable pour le super-admin main. Source : src/routes/audit.js, table audit_logs.

Schéma des entrées

ChampDescription
actor_id + actor_emailUtilisateur qui a fait l'action. actor_id=0 = system / API key tenant.
actioncreate · update · delete · login · logout · password_reset · password_setup · …
resource_typelink · user · tenant · settings · banner · workflow · campaign · referral · …
resource_idid numérique ou slug de la ressource affectée.
diffJSON {field: {old, new}}. Vide pour create/delete (état complet ailleurs).
ipIP du client (si JWT request) ou null (si API key).
created_atTimestamp UTC.

API

GET /api/v1/audit?limit=50&offset=0&resource_type=link&action=delete Auth required + admin — liste paginée tenant-scoped

Filtres query (tous combinables, optionnels) :

  • resource_type : link, user, settings, tenant, banner, campaign, workflow, …
  • action : create, update, delete, login, …
  • limit : ≤500 (default 50)
  • offset : pagination

Tri : created_at DESC (plus récent en premier).

3 cas d'usage concrets

Cas 1 — "Qui a modifié les paramètres SMTP la semaine dernière ?"

# Filtre resource_type + action + post-traitement par date côté client
curl -H "Authorization: Bearer $JWT" \
  "$BASE/api/v1/audit?resource_type=settings&action=update&limit=200" \
  | jq '.logs[] | select(.created_at > "2026-04-25" and (.diff | tostring | contains("smtp")))'

Résultat : liste des entrées avec actor_email, diff (champs SMTP modifiés old→new), timestamp. Si vide → personne n'a touché. Sinon vous savez qui prévenir.

Cas 2 — "Qui a créé ce lien Black Friday ?"

# Recherche par resource_id (slug du lien)
curl -H "Authorization: Bearer $JWT" \
  "$BASE/api/v1/audit?resource_type=link&action=create&limit=500" \
  | jq '.logs[] | select(.resource_id == "blackfriday-2026")'

Résultat : 1 entrée avec actor_email + diff (champs initiaux du lien) + ip. Si plusieurs → plusieurs créations historiques (le slug a peut-être été supprimé puis recréé).

Cas 3 — "Qui a supprimé la campagne X ?"

# action=delete + resource_type=campaign
curl -H "Authorization: Bearer $JWT" \
  "$BASE/api/v1/audit?resource_type=campaign&action=delete&limit=100" \
  | jq '.logs[]'

Sur les ressources delete, le diff capture l'état avant suppression (utile pour reconstruire si besoin). L'IP indique si c'était fait depuis le dashboard ou un script.

Bonnes pratiques investigation

Cf. également errors catalog pour les codes applicatifs ; architecture.html pour le data model audit_logs.

Testing patterns — comment tester son intégration

Trois approches recommandées selon le besoin : (1) tenant sandbox sur l'instance partagée pour les tests end-to-end manuels, (2) mock SDK pour les tests unitaires côté client, (3) fixtures HTTP pour les tests d'intégration côté backend qui consomment l'API DeepLink.

1. Tenant sandbox — tests E2E manuels

Chaque tenant peut activer un mode sandbox via Settings → Advanced → Sandbox mode. Les liens créés en sandbox ont un préfixe distinct (sbx_) et leurs events sont isolés des stats de prod. Idéal pour valider une intégration avant le go-live.

2. Mock SDK — tests unitaires

Quand on teste son code applicatif (ex. logique post-deeplink-resolve), on ne veut pas que les tests fassent de vrais appels HTTP au serveur. Stub le SDK : retourner une réponse synchrone canned.

Node.js — Jest / Vitest

// __tests__/deeplink.test.js
const { jest } = require('@jest/globals');
const deeplink = require('@akeeli/deeplink-sdk-node');

jest.mock('@akeeli/deeplink-sdk-node');

deeplink.resolveLink.mockResolvedValue({
  link_id:  'lnk_abc',
  campaign: 'spring-2026',
  payload:  { sku: 'XYZ-42' }
});

test('handler reads campaign from resolved link', async () => {
  const r = await handler({ headers: { 'x-deeplink-id': 'lnk_abc' } });
  expect(r.campaign).toBe('spring-2026');
});

Python — pytest + responses

import responses
from deeplink import Client

@responses.activate
def test_resolve_link():
    responses.add(
        responses.GET,
        "https://api.deeplink.akeeli.dev/api/v1/links/lnk_abc",
        json={"link_id": "lnk_abc", "campaign": "spring-2026"},
        status=200,
    )
    c = Client(api_key="sbx_test")
    r = c.resolve_link("lnk_abc")
    assert r.campaign == "spring-2026"

Swift / iOS — XCTest + URLProtocol stub

import XCTest
@testable import AkeeliDeepLink

class MockURLProtocol: URLProtocol {
  static var stub: (Data, HTTPURLResponse)?
  override class func canInit(with: URLRequest) -> Bool { true }
  override func startLoading() {
    guard let (data, resp) = Self.stub else { return }
    client?.urlProtocol(self, didReceive: resp, cacheStoragePolicy: .notAllowed)
    client?.urlProtocol(self, didLoad: data)
    client?.urlProtocolDidFinishLoading(self)
  }
  override func stopLoading() {}
}

func testResolveLink() async throws {
  let body = #"{"link_id":"lnk_abc","campaign":"spring-2026"}"#.data(using: .utf8)!
  MockURLProtocol.stub = (body, HTTPURLResponse(...)!)
  let result = try await DeepLink.resolve(id: "lnk_abc")
  XCTAssertEqual(result.campaign, "spring-2026")
}

3. Fixtures HTTP — tests d'intégration côté serveur

Si ton backend appelle l'API DeepLink (ex. webhook receiver, batch import), enregistre les réponses réelles via nock (Node) / vcr.py (Python) lors du premier run, puis rejoue ensuite hors-ligne.

// Node — nock + recorded fixture
const nock = require('nock');
nock.back.fixtures = './tests/fixtures';
nock.back.setMode('record');          // au 1er run
nock.back.setMode('lockdown');        // CI : interdit le réseau, replay only

const { nockDone } = await nock.back('resolve_link.json');
const link = await deeplink.resolveLink('lnk_abc');
nockDone();

Bonnes pratiques

Cf. errors catalog pour la liste des codes attendus quand on simule des cas d'erreur ; tests/ côté serveur pour des exemples de fixtures réelles.

Troubleshooting

Universal Links don't open the iOS app

App Links don't open the Android app

Deferred deep link match fails

Webhook never reaches our endpoint

Custom domain stuck in "ssl_issuing"

Logs don't show my error