Delegate
Documentation

Integrate Delegate.

Verify users by their actual financial reality — bank account history, payroll, name match. Returns a verdict in under 1 second. Every decision is written to a tamper-evident audit log your auditors can verify independently.

Integration overview

Three small pieces. Most teams ship in a single afternoon.

01 · BACKEND
Mint a link token
When the user reaches a moment that matters, your server requests a short-lived link token from Delegate.
02 · FRONTEND
User connects their bank
Plaid Link opens. User picks bank, logs in, grants access. ~30 seconds. You never see their credentials.
03 · BACKEND
Get the verdict
Delegate scores 8 financial signals and returns allow, review, or block. Logged to your audit chain.
Stack: Any backend (Node, Python, Go, Ruby, PHP, .NET — any HTTP client)
Frontend: 1 script tag, ~30KB
Time to first verdict: ~30 minutes

Your API keys

API keys authenticate every request. Treat them like passwords. Keys are shown only once at creation — save them in your secrets manager. If a key leaks, revoke it immediately.

Quickstart

The fastest path to a working Financial Reality verification — the core of Delegate. Most teams go from zero to live verdict in about 30 minutes of integration work.

Even faster: use the JS SDK. If you'd rather skip the manual integration below, drop our SDK into your page and verify with one line:
<script src="https://testdelegate.com/delegate.js"></script>
<script>
  Delegate.init({ proxyBase: '/api/delegate' });
  // later, when user reaches a moment that matters:
  const result = await Delegate.verifyFinancial({ userId, userName });
  if (result.verdict === 'allow') { /* proceed */ }
</script>
The SDK loads Plaid Link automatically and handles the full flow. You still need a small backend proxy (~15 lines) so your API key never reaches the browser:
// Backend: Express proxy at /api/delegate/*
const DELEGATE = 'https://delegate-api-production-45ec.up.railway.app';

['/link-token', '/exchange'].forEach(path => {
  app.post('/api/delegate' + path, async (req, res) => {
    const r = await fetch(DELEGATE + '/v1/financial-verify' + path, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', 'X-API-Key': process.env.DELEGATE_API_KEY },
      body: JSON.stringify(req.body),
    });
    res.status(r.status).json(await r.json());
  });
});
That's the entire backend. The SDK calls these two endpoints from the browser; this proxy forwards them with your API key attached. Manual integration below shows what the SDK does under the hood if you'd rather skip the SDK entirely.
Three pieces total. One frontend script (Plaid Link). Two backend endpoints (/link-token mints a session, /exchange finalizes it and returns the verdict). Your application decides what to do with the verdict.
1
Mint a link token (server → server)

When your user reaches the moment that matters — signup, first investment, withdrawal above a threshold — your backend asks Delegate for a link token. The token is short-lived and scoped to one user.

javascript (Node.js / backend)
// Backend: POST /your-app/start-verification
app.post('/start-verification', async (req, res) => {
  const response = await fetch(
    'https://delegate-api-production-45ec.up.railway.app/v1/financial-verify/link-token',
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-API-Key': 'YOUR_API_KEY',
      },
      body: JSON.stringify({
        end_user_id: req.user.id,           // your user identifier
        context: 'investment_signup',    // optional metadata
      }),
    }
  );
  const { link_token } = await response.json();
  res.json({ link_token });
});
2
Open Plaid Link in your frontend

Use the Plaid Link script (CDN, ~30KB). Pass the link token your backend just minted. The user picks their bank, logs in, and grants access — all inside Plaid's UI. You never see their bank credentials.

html (your signup page)
<script src="https://cdn.plaid.com/link/v2/stable/link-initialize.js"></script>
<script>
async function verifyUser() {
  // 1. Ask your backend for a link token
  const { link_token } = await fetch('/start-verification', { method: 'POST' })
    .then(r => r.json());

  // 2. Open Plaid Link with that token
  const handler = Plaid.create({
    token: link_token,
    onSuccess: async (publicToken) => {
      // 3. Send publicToken to your backend to finalize (next step)
      await fetch('/finalize-verification', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ public_token: publicToken }),
      });
    },
  });
  handler.open();
}
</script>
3
Finalize and get the verdict (server → server)

Send the public_token back to Delegate. We exchange it with Plaid, fetch account data, run all 8 scoring signals, and return the verdict in under a second.

javascript (Node.js / backend)
// Backend: POST /your-app/finalize-verification
app.post('/finalize-verification', async (req, res) => {
  const response = await fetch(
    'https://delegate-api-production-45ec.up.railway.app/v1/financial-verify/exchange',
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-API-Key': 'YOUR_API_KEY',
      },
      body: JSON.stringify({
        public_token: req.body.public_token,
        end_user_id: req.user.id,
        signup_data: { name: req.user.name },  // for name-match scoring
        context: 'investment_signup',
      }),
    }
  );
  const { score, verdict, reasons } = await response.json();

  if (verdict === 'block') {
    return res.status(403).json({ error: 'Verification failed', reasons });
  }
  if (verdict === 'review') {
    await flagForManualReview(req.user.id, score, reasons);
    return res.json({ status: 'pending_review' });
  }
  // verdict === 'allow' → proceed with the action
  await approveAction(req.user.id);
  res.json({ status: 'approved', score });
});

That's it. Three endpoints, one frontend script. Verdict in under a second. Every decision auto-written to your hash-chained audit log — auditors can verify integrity independently.

💡
Sandbox is identical to production. The flow above works the same in sandbox (free, fake banks) as in production (real banks, paid). When you're ready, swap your sandbox API key for a production one. Zero code changes.

Authentication

Every request requires the X-API-Key header. Keys look like dlg_live_* followed by 40 hex characters. Generate yours from the section above.

Never expose live keys to the browser. All Delegate API calls (/link-token, /exchange, /verify) must happen from your backend. The Plaid Link script in the browser only ever sees the short-lived link_token — never your API key.

What to do if a key leaks

Revoke immediately from the section above. Once revoked, the key is rejected on every subsequent request within seconds. Generate a new one and roll your environment variable.

Step 1: mint a Plaid link token. Your backend calls this, then your frontend uses the token to open Plaid Link.

curl
curl https://delegate-api-production-45ec.up.railway.app/v1/financial-verify/link-token \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_API_KEY" \
  -d '{
    "end_user_id": "user_abc123",
    "context": "investment_signup"
  }'

Returns:

{
  "link_token": "link-sandbox-abc123...",
  "expiration": "2026-05-06T03:30:00Z"
}

From your frontend, pass that link_token to Plaid Link:

javascript
const handler = Plaid.create({
  token: linkToken,
  onSuccess: async (publicToken) => {
    // Send publicToken to your backend (step 2)
    await fetch('/your-backend/finalize-verification', {
      method: 'POST',
      body: JSON.stringify({ public_token: publicToken, user_id: '...' })
    });
  },
});
handler.open();

POST /v1/financial-verify/exchange

Step 2: exchange the public_token for the verification result. Backend-to-backend call. Returns the score, verdict, and full reason breakdown.

curl
curl https://delegate-api-production-45ec.up.railway.app/v1/financial-verify/exchange \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_API_KEY" \
  -d '{
    "public_token": "public-sandbox-...",
    "end_user_id": "user_abc123",
    "signup_data": {
      "name": "John Smith",
      "email": "john@example.com"
    },
    "context": "investment_signup"
  }'

Returns:

{
  "verification_id": 42,
  "score": 87,
  "verdict": "allow",
  "scoring_version": "financial-rules-2026.05",
  "reasons": [
    { "code": "ACCOUNT_TYPE_CHECKING", "impact": +5,
      "detail": "Standard checking account" },
    { "code": "BALANCE_HIGH", "impact": +12,
      "detail": "Available balance $5,500.42" },
    { "code": "NAME_MATCH", "impact": +20,
      "detail": "Account holder name matches signup name" },
    { "code": "TX_VOLUME_HIGH", "impact": +8,
      "detail": "120 transactions in 90 days" },
    { "code": "PAYROLL_DETECTED", "impact": +15,
      "detail": "Recurring payroll deposits detected" }
  ]
}

Acting on the verdict

Three verdicts. You decide what each one means in your application:

Scoring signals

The Financial Reality Score is the sum of impacts from these signals. Versioned (current: financial-rules-2026.05) so historical decisions remain reproducible.

Signal Range What it catches
ACCOUNT_TYPE-25 to +5Prepaid cards (synthetic identity favorite); checking/savings preferred
BALANCE-8 to +12Drained accounts; just-funded mules; healthy real-life balances
NAME_MATCH-30 to +20Strongest synthetic-identity catcher: bank account holder name vs signup name
TX_VOLUME-15 to +8Real users have lots of transactions; new accounts and mules don't
PAYROLL_DETECTED-5 to +15Recurring W-2 payroll = real person with real job. Synthetics rarely have this.
MERCHANT_DIVERSITY-5 to +6Real life means many different merchants — gas, groceries, Amazon, restaurants
ACCOUNT_AGE-20 to +5Inferred from oldest transaction. Brand-new accounts (under 14 days) are highly suspect.

Every reason is itemized in the response, written to the audit log, and surfaced in your dashboard. Auditors reviewing your fraud controls can see exactly why each decision was made — the cryptographic chain proves it wasn't tampered with after the fact.

POST /v1/verify

Supplementary detection — used to gather device, network, and behavioral signals at signup. This runs alongside Financial Reality verification (when you call it) and contributes to a separate fraud risk score.

curl
curl https://delegate-api-production-45ec.up.railway.app/v1/verify \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_API_KEY" \
  -d '{
    "fingerprint": "fp_abc123",
    "email": "user@example.com",
    "webdriver": false,
    "form_fill_time_ms": 4200,
    "mouse_movements": 32,
    "typing_events": 28,
    "paste_events": 0
  }'

Response

json
{
  "verdict": "allow",
  "score": 14,
  "reasons": [],
  "agent_type": "human"
}

POST /v1/score

Continuous scoring for existing users. Use after login or before high-risk actions (transfers, withdrawals, account changes) to update the user's trust score.

curl
curl https://delegate-api-production-45ec.up.railway.app/v1/score \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_API_KEY" \
  -d '{
    "user_id": "your_internal_user_id",
    "event": "login",
    "fingerprint": "fp_abc123",
    "ip": "203.0.113.42"
  }'

Signals the SDK collects

Behavioral and device signals captured silently as the user fills your form. No user-visible UI. No additional steps for the user.

SignalWhat it tells us
fingerprintStable device identifier across visits
emailDomain analysis (disposable / role-based)
webdriverWhether the browser is automated (Selenium, Puppeteer, etc.)
form_fill_time_msHow long the user took to fill the form
mouse_movementsTotal mouse events during the session
typing_eventsTotal keypress events
paste_eventsHow many times the user pasted into a field
mouse_entropyVariance in mouse movement (synthetic vs human)
keystroke_intervals_msTime between keypresses (rhythm = human)
screen / timezone / languagesDevice environment context

Verdicts

Three possible outcomes. How you handle each is up to your product, but here's our recommendation.

allow  Score 0–35. Proceed with the signup as normal.

challenge  Score 36–69. Add friction: SMS code, email verification, or step-up KYC. Auto-creates a case in your workspace.

block  Score 70+. Reject the signup. Auto-creates a high-priority case routed to your fraud specialist.

Auto-created cases

Every challenge or block verdict spawns a case in your workspace automatically. You don't need to do anything — open Workspace and your team's queue is already populated.

Each case includes the original signals, AI-generated reasoning for routing, and an activity log that updates as your team triages.

Webhooks

Get notified when a case is created, assigned, or resolved. Configure endpoints in Settings → Webhooks. Events emitted: case.created, case.assigned, case.resolved, verification.completed, financial_verification.completed.

Webhook payloads are HMAC-SHA256 signed. Verify the X-Delegate-Signature header against your shared secret to confirm authenticity.

Common questions

How long does integration actually take?
A single engineer can ship a working Financial Reality verification in an afternoon. Three components: one frontend script (~30KB Plaid Link, drop-in), two backend endpoints (link-token mint, exchange). No SDK to install, no configuration files, no schema migrations on your side. Most teams are testing against sandbox within an hour.
What stack do we need to support this?
Any backend that can make HTTPS requests — Node, Python, Go, Ruby, PHP, .NET, Java, anything. The API is plain JSON over HTTP. Frontend needs to load the Plaid Link script (one tag in <head>). No framework requirements; works with React, Vue, plain HTML, native iOS/Android apps via Plaid's mobile SDKs.
What about existing KYC providers we already use?
Delegate runs alongside your existing KYC. We don't replace document verification, OFAC screening, or PEP checks — we add a financial reality layer that catches synthetic identities and AI-driven fraud that gets through document-based KYC. Most customers run Delegate after document KYC: if the documents pass, then we verify financial reality. Failures at either layer block the user.
What happens to bank data after a verification?
Delegate stores only what's needed to reproduce the score: a summary of the signals (account type, balance bucket, transaction count, name match result, payroll detection). Raw transaction descriptions, account numbers, and credentials are never stored. The Plaid access token is retained only if you opt into continuous monitoring; otherwise we revoke it after scoring. Full breakdown — what we collect, where it lives, who has access, retention, subprocessors — is on the security page.
Where can I review your security posture and subprocessors?
The full security and data-handling page covers what we collect, where it lives (Railway-managed Postgres, US-East), how it's encrypted (AES-256 at rest, TLS 1.3 in transit, field-level AES-256-GCM rolling out for sensitive fields), who has access, retention windows, every subprocessor (Plaid, Railway, Resend, Anthropic, Stripe), and our compliance posture. We are SOC 2 audit-ready (Type II audit pending first paying customer at scale) — we have not claimed certifications we don't have.
What about users who don't have a bank account or won't link?
You decide. Common patterns: (1) require for high-value actions only — let users browse without it, (2) lower-trust mode — verified users get higher limits, unverified can still transact within smaller bounds, (3) escalate to manual review — flag the account, route to your ops team. Delegate gives you the verdict; your application chooses the policy.
How do we move from sandbox to production?
Two changes: (1) update Plaid environment variables on Delegate's backend from sandbox to production (after Plaid approves your production application — typically 3-7 days), (2) generate a production API key in Delegate. Zero code changes. The API contract is identical between sandbox and production. We recommend running both environments in parallel for the first week.
What's the latency for a verification?
The user experience: Plaid Link takes 25-45 seconds for the median user (entering bank credentials, picking accounts, confirming). Once the public_token reaches our API, scoring takes 600-900ms (parallel calls to fetch accounts, identity, and transactions, then deterministic scoring). Total time from "user clicks verify" to "verdict returned" is typically 30-60 seconds.
How do auditors verify the integrity of our records?
Every verification writes one row to your hash-chained audit log. Each row contains the previous row's SHA-256 hash, plus a hash of itself. Tampering with any row breaks the chain — verifiable independently with no trust in Delegate. Your auditor gets a read-only portal login, sees the full log, can export it as a signed file, and can reverify the chain offline using any standard SHA-256 implementation.
What if Delegate goes down?
Your application controls the failure mode. Common patterns: (1) fail-closed — if Delegate is unreachable, treat as review and queue for manual processing, (2) fail-open — if Delegate is unreachable for low-risk actions, allow with a flag, (3) circuit breaker — switch to a backup verification method after N failures. Production uptime SLA is included in Compliance tier contracts.