All checks were successful
Build and Deploy / deploy (push) Successful in 49s
- main.py: neutral API description (remove 'sales demo') - base.html: 'Open Banking Demo' nav, neutral footer - demo.py: /debug-session gated behind DEMO_MODE, all print() → logging, webhook receiver has C# signature verification example + Tink docs link, removed duplicate import - demo_data.py: all hardcoded 2026 dates replaced with dynamic date helpers - step2.html: external_user_id terminology (remove tink_external_ref) - step.html: sandbox note on Step 3 (anonymous vs production flow) - step6.html: 'Next Steps' section for C#/.NET implementation
272 lines
13 KiB
HTML
272 lines
13 KiB
HTML
{% extends "base.html" %}
|
||
{% block title %} — Step 6: Webhooks & Events{% endblock %}
|
||
|
||
{% block stepper %}
|
||
<div class="bg-slate-900/60 border-b border-slate-800">
|
||
<div class="max-w-6xl mx-auto px-4 py-3">
|
||
<div class="flex items-center gap-1 overflow-x-auto">
|
||
{% set step_names = ["Auth", "Opret Bruger", "Tilslut Bank", "Konti", "Transaktioner", "Webhooks"] %}
|
||
{% for i in range(1, 7) %}
|
||
<a href="/demo/step/{{ i }}"
|
||
class="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm whitespace-nowrap transition
|
||
{% if i == 6 %}bg-violet-600 text-white font-semibold
|
||
{% elif i < 6 %}text-slate-300 hover:text-white
|
||
{% else %}text-slate-600 hover:text-slate-400{% endif %}">
|
||
<span class="w-5 h-5 rounded-full text-xs font-bold flex items-center justify-center
|
||
{% if i < 6 %}bg-slate-700 text-slate-300
|
||
{% elif i == 6 %}bg-violet-500 text-white
|
||
{% else %}bg-slate-800 text-slate-600{% endif %}">{{ i }}</span>
|
||
{{ step_names[i-1] }}
|
||
</a>
|
||
{% if i < 6 %}
|
||
<span class="text-slate-700">›</span>
|
||
{% endif %}
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="mb-6">
|
||
<div class="flex items-center gap-2 mb-1">
|
||
<span class="w-8 h-8 rounded-full bg-violet-600 text-white text-sm font-bold flex items-center justify-center">6</span>
|
||
<div>
|
||
<h2 class="text-xl font-bold text-white">Webhooks & Real-time Events</h2>
|
||
<p class="text-slate-400 text-sm">Push-notifikationer til din backend</p>
|
||
</div>
|
||
</div>
|
||
<p class="text-slate-400 text-sm mt-2 max-w-2xl">
|
||
Registrér dit endpoint hos Tink — så sender de automatisk en HTTP POST til dig
|
||
hver gang en transaktion bogføres, opdateres eller en konto ændres. Zero polling.
|
||
</p>
|
||
</div>
|
||
|
||
{% if error %}
|
||
<div class="bg-red-950/50 border border-red-800/50 rounded-xl p-5 mb-6">
|
||
<p class="text-red-300 text-sm font-semibold mb-1">Webhook API fejl</p>
|
||
<pre class="text-red-400 text-sm font-mono whitespace-pre-wrap">{{ error }}</pre>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- How it works banner -->
|
||
<div class="bg-slate-900/50 border border-violet-800/30 rounded-xl p-5 mb-6">
|
||
<h3 class="text-white font-semibold mb-3 text-sm">Hvordan webhooks virker</h3>
|
||
<div class="flex flex-wrap items-center gap-2 text-sm">
|
||
<div class="bg-slate-800 rounded-lg px-3 py-2 text-slate-300">
|
||
<span class="text-violet-400 font-mono text-xs block mb-0.5">Din app</span>
|
||
Brugeren kobler bank
|
||
</div>
|
||
<span class="text-slate-600 text-lg">→</span>
|
||
<div class="bg-slate-800 rounded-lg px-3 py-2 text-slate-300">
|
||
<span class="text-emerald-400 font-mono text-xs block mb-0.5">Tink</span>
|
||
Henter transaktioner
|
||
</div>
|
||
<span class="text-slate-600 text-lg">→</span>
|
||
<div class="bg-violet-900/50 border border-violet-700/50 rounded-lg px-3 py-2 text-slate-200 font-semibold">
|
||
<span class="text-violet-300 font-mono text-xs block mb-0.5">POST /webhooks/tink</span>
|
||
Dit endpoint modtager event
|
||
</div>
|
||
<span class="text-slate-600 text-lg">→</span>
|
||
<div class="bg-slate-800 rounded-lg px-3 py-2 text-slate-300">
|
||
<span class="text-amber-400 font-mono text-xs block mb-0.5">Din app</span>
|
||
Opdaterer UI / notifikation
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||
|
||
<!-- List webhooks -->
|
||
<div class="bg-slate-900 border border-slate-800 rounded-xl overflow-hidden">
|
||
<div class="px-4 py-3 border-b border-slate-800">
|
||
<div class="flex items-center justify-between">
|
||
<div>
|
||
<span class="text-sm font-semibold text-white">Registrerede webhooks</span>
|
||
<code class="block text-xs text-emerald-400 font-mono mt-0.5">GET /api/v1/webhooks</code>
|
||
</div>
|
||
<span class="text-xs px-2 py-0.5 rounded-full bg-slate-800 text-slate-400 border border-slate-700 font-mono">app token</span>
|
||
</div>
|
||
</div>
|
||
<div class="px-4 py-3 border-b border-slate-800 bg-slate-950/40">
|
||
<div class="flex items-center justify-between mb-2">
|
||
<span class="text-xs text-slate-500 uppercase tracking-wider">cURL</span>
|
||
<button onclick="copyToClipboard('curl-list')"
|
||
class="text-xs text-slate-400 hover:text-white transition px-2 py-1 rounded bg-slate-800">Kopier</button>
|
||
</div>
|
||
<pre id="curl-list" class="text-xs text-amber-300 font-mono whitespace-pre-wrap">{{ curl_list }}</pre>
|
||
</div>
|
||
<div class="p-4 max-h-80 overflow-y-auto">
|
||
{% if result_webhooks %}
|
||
{% if is_demo %}<p class="text-xs text-amber-400/70 mb-2 font-semibold">⚠ Sample Data</p>{% endif %}
|
||
<pre class="raw-json">{{ result_webhooks | tojson(indent=2) }}</pre>
|
||
{% else %}
|
||
<p class="text-slate-500 text-sm text-center py-6">Ingen webhooks registreret endnu.</p>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Register webhook -->
|
||
<div class="bg-slate-900 border border-slate-800 rounded-xl overflow-hidden">
|
||
<div class="px-4 py-3 border-b border-slate-800">
|
||
<div class="flex items-center justify-between">
|
||
<div>
|
||
<span class="text-sm font-semibold text-white">Registrér webhook endpoint</span>
|
||
<code class="block text-xs text-emerald-400 font-mono mt-0.5">POST /api/v1/webhooks</code>
|
||
</div>
|
||
<span class="text-xs px-2 py-0.5 rounded-full bg-emerald-900/50 text-emerald-300 border border-emerald-800/50 font-mono">registreret ✓</span>
|
||
</div>
|
||
</div>
|
||
<div class="px-4 py-3 border-b border-slate-800 bg-slate-950/40">
|
||
<div class="flex items-center justify-between mb-2">
|
||
<span class="text-xs text-slate-500 uppercase tracking-wider">cURL</span>
|
||
<button onclick="copyToClipboard('curl-register')"
|
||
class="text-xs text-slate-400 hover:text-white transition px-2 py-1 rounded bg-slate-800">Kopier</button>
|
||
</div>
|
||
<pre id="curl-register" class="text-xs text-amber-300 font-mono whitespace-pre-wrap">{{ curl_register }}</pre>
|
||
</div>
|
||
<div class="p-4 max-h-80 overflow-y-auto">
|
||
{% if webhook_registered %}
|
||
{% if is_demo %}<p class="text-xs text-amber-400/70 mb-2 font-semibold">⚠ Sample Data</p>{% endif %}
|
||
<pre class="raw-json">{{ webhook_registered | tojson(indent=2) }}</pre>
|
||
{% else %}
|
||
<p class="text-slate-500 text-sm text-center py-6">Webhook ikke registreret.</p>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- Sample event payload -->
|
||
<div class="mt-6 bg-slate-900 border border-slate-800 rounded-xl overflow-hidden">
|
||
<div class="px-4 py-3 border-b border-slate-800">
|
||
<div class="flex items-center justify-between">
|
||
<div>
|
||
<span class="text-sm font-semibold text-white">Sample Webhook Payload</span>
|
||
<p class="text-xs text-slate-400 mt-0.5">Sådan ser en event ud når Tink poster til dit endpoint</p>
|
||
</div>
|
||
<span class="text-xs px-2 py-0.5 rounded-full bg-amber-900/50 text-amber-300 border border-amber-800/50 font-mono">incoming POST</span>
|
||
</div>
|
||
</div>
|
||
<div class="p-4 overflow-x-auto">
|
||
<pre class="text-xs text-emerald-300 font-mono whitespace-pre">{
|
||
"event": "account-booked-transaction:created",
|
||
"context": {
|
||
"userId": "a8b3c2d1-...",
|
||
"externalUserId": "moneycapp-user-42"
|
||
},
|
||
"content": {
|
||
"id": "tx_9f3a2b1c...",
|
||
"accountId": "acc_7e1d4f2a...",
|
||
"amount": {
|
||
"currencyCode": "DKK",
|
||
"value": { "scale": 2, "unscaledValue": "-24900" }
|
||
},
|
||
"dates": {
|
||
"booked": "2025-05-22",
|
||
"value": "2025-05-22"
|
||
},
|
||
"descriptions": {
|
||
"display": "Netto Albertslund",
|
||
"original": "NETTO ALBERTSLUND"
|
||
},
|
||
"merchantInformation": {
|
||
"merchantCategoryCode": "5411",
|
||
"merchantName": "Netto"
|
||
},
|
||
"status": "BOOKED",
|
||
"types": { "type": "DEFAULT" }
|
||
}
|
||
}</pre>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Your receiver endpoint -->
|
||
<div class="mt-4 bg-slate-900/60 border border-slate-700/50 rounded-xl p-4">
|
||
<div class="flex items-center gap-3 mb-3">
|
||
<div class="w-8 h-8 rounded-full bg-emerald-600/20 border border-emerald-600/40 flex items-center justify-center">
|
||
<svg class="w-4 h-4 text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M12 5l7 7-7 7"/>
|
||
</svg>
|
||
</div>
|
||
<div>
|
||
<p class="text-white text-sm font-semibold">Dit webhook modtager endpoint er live</p>
|
||
<code class="text-emerald-400 text-xs font-mono">POST {{ app_base_url }}/webhooks/tink</code>
|
||
</div>
|
||
</div>
|
||
<details class="text-sm text-slate-400">
|
||
<summary class="cursor-pointer hover:text-white transition text-xs font-semibold uppercase tracking-wider text-slate-500">Se receiver kode</summary>
|
||
<div class="mt-3 bg-slate-950 rounded-lg p-4 overflow-x-auto">
|
||
<pre class="text-xs text-emerald-300 font-mono whitespace-pre">@router.post("/webhooks/tink")
|
||
async def tink_webhook(request: Request):
|
||
payload = await request.json()
|
||
event_type = payload.get("event", "unknown")
|
||
content = payload.get("content", {})
|
||
|
||
# Her ville MoneyCapp opdatere sin database,
|
||
# sende push-notifikation, opdatere UI via SSE etc.
|
||
|
||
print(f"Tink event: {event_type}")
|
||
print(f"Transaction ID: {content.get('id')}")
|
||
print(f"Amount: {content.get('amount')}")
|
||
|
||
return {"status": "received"}</pre>
|
||
</div>
|
||
</details>
|
||
</div>
|
||
|
||
<!-- Final CTA -->
|
||
<div class="mt-8 bg-gradient-to-br from-violet-900/30 to-indigo-900/20 border border-violet-700/30 rounded-2xl p-8 text-center">
|
||
<div class="text-4xl mb-3">🎉</div>
|
||
<h3 class="text-2xl font-bold text-white mb-2">Det var hele flowet</h3>
|
||
<p class="text-slate-400 mb-2 max-w-lg mx-auto">
|
||
Fra brugeroprettelse over live bankdata til real-time webhooks — alt via Tink API.
|
||
</p>
|
||
<p class="text-slate-500 text-sm mb-6 max-w-xl mx-auto">
|
||
Auth → bruger → bank-tilslutning → konti (v2) → transaktioner (v2) → webhooks.
|
||
</p>
|
||
<div class="flex gap-3 justify-center flex-wrap">
|
||
<a href="/demo/reset" class="px-5 py-2.5 border border-slate-600 text-slate-300 hover:text-white hover:border-slate-400 rounded-xl text-sm transition">↺ Kør demo igen</a>
|
||
<a href="https://docs.tink.com/api-introduction" target="_blank"
|
||
class="px-5 py-2.5 bg-violet-600 hover:bg-violet-500 text-white rounded-xl text-sm font-semibold transition">
|
||
Tink Docs →
|
||
</a>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Next Steps for implementation -->
|
||
<div class="mt-6 bg-slate-900/60 border border-slate-800 rounded-2xl p-7">
|
||
<h4 class="text-white font-semibold text-base mb-4 flex items-center gap-2">
|
||
<svg class="w-4 h-4 text-violet-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/></svg>
|
||
Næste skridt mod en produktion-klar integration
|
||
</h4>
|
||
<div class="grid md:grid-cols-2 gap-3 text-sm text-slate-400">
|
||
<div class="bg-slate-800/60 rounded-lg p-4 space-y-1.5">
|
||
<p class="text-violet-300 font-semibold text-xs uppercase tracking-wider mb-2">Backend (C# / .NET)</p>
|
||
<p>1. Lav en <code class="text-violet-300 font-mono text-xs">TinkApiClient</code> wrapper (brug <code class="text-xs font-mono">tink/client.py</code> som reference)</p>
|
||
<p>2. Gem <code class="text-violet-300 font-mono text-xs">external_user_id</code> + <code class="text-violet-300 font-mono text-xs">user_id</code> i din kundedatabase</p>
|
||
<p>3. Implementér <code class="text-violet-300 font-mono text-xs">/callback</code> endpoint med token exchange</p>
|
||
<p>4. Gem tokens sikkert (encrypted, server-side — ikke i cookie)</p>
|
||
</div>
|
||
<div class="bg-slate-800/60 rounded-lg p-4 space-y-1.5">
|
||
<p class="text-violet-300 font-semibold text-xs uppercase tracking-wider mb-2">Webhooks & Production</p>
|
||
<p>5. Byg webhook receiver med <a href="https://docs.tink.com/api#webhook/webhook-endpoints" target="_blank" class="text-violet-400 hover:text-violet-300 underline">HMAC-SHA256 signature verification</a></p>
|
||
<p>6. Skift til production Tink-credentials (Tink Console)</p>
|
||
<p>7. Registrér din production callback URI i Tink Console</p>
|
||
<p>8. Brug <code class="text-violet-300 font-mono text-xs">authorization-grant/delegate</code> i prod-flowet (ikke anon)</p>
|
||
</div>
|
||
</div>
|
||
<p class="text-xs text-slate-600 mt-4">
|
||
Kildekoden til dette demo (<code class="font-mono">src/tink/client.py</code> og <code class="font-mono">src/routes/demo.py</code>) er skrevet for at være letlæselig og direkte overførbar til andre platforme.
|
||
</p>
|
||
</div>
|
||
|
||
<!-- Navigation -->
|
||
<div class="mt-4 flex justify-start">
|
||
<a href="/demo/step/5"
|
||
class="px-4 py-2.5 border border-slate-700 text-slate-300 hover:text-white hover:border-slate-500 rounded-xl text-sm transition">
|
||
← Step 5
|
||
</a>
|
||
</div>
|
||
{% endblock %}
|