Gareth Wilson Gareth Wilson

Guide to Tikkie Webhooks: Features and Best Practices

Published


Tikkie is ABN AMRO's payment-request product: a business creates a Tikkie, shares the link, and the customer pays it. Tikkie notifications tell your systems when one of those payment requests is paid, when a refund on a payment is executed, and when a transaction bundle is paid out. If you're marking orders as paid, recording refunds, or reconciling payouts in an accounting system, notifications are how you find out without polling every open request.

This guide covers how Tikkie API v2 notifications work, how to subscribe to them through the API, why they are unsigned and what to do about it, the platform's delivery limits, and the best practices for production.

What are Tikkie webhooks?

Tikkie webhooks (Tikkie calls them notifications) are HTTP callbacks from the Tikkie API v2. Once you register a subscription, Tikkie POSTs a JSON body to your URL when a subscribed event happens. The body is small and flat: a subscriptionId, a notificationType naming the event, and one or more tokens that identify the payment, refund, or bundle.

A Tikkie notification carries nothing else: no amount, status, timestamp, or event id. It tells you that something happened and where to look; the facts come from a follow-up call to the Tikkie API. This scope covers the Tikkie API v2 only, not ABN AMRO's separate Business Account Notification API or the retired Tikkie v1 API, which use different schemes.

Tikkie webhook features

FeatureDetails
ConfigurationAPI only: POST /paymentrequestssubscription (payments and refunds) and POST /transactionssubscription (transaction bundles)
AuthenticationNone. No signature header, shared secret, HMAC, timestamp header, or published IP allowlist
EnvelopeFlat JSON: subscriptionId, notificationType, plus the tokens for that type
Event typenotificationType body field (PAYMENT, REFUND, BUNDLE); there is no event-type header
DeliveryHTTP POST with Content-Type: application/json; any 2XX acknowledges
RetriesBest effort, with a maximum of three attempts
SubscriptionsOne active subscription per type; a repeat POST overwrites the existing one, URL included
API credentialsAPI-Key (ABN AMRO developer portal consumer key) and X-App-Token (Tikkie app token) on every call
EnvironmentsProduction https://api.abnamro.com/v2/tikkie, sandbox https://api-sandbox.abnamro.com/v2/tikkie

Common events

Tikkie has three notification types, split across two subscriptions:

EventFires when
PAYMENTA payment is made on one of your payment requests (carries paymentRequestToken and paymentToken)
REFUNDA refund on a payment is executed (adds refundToken)
BUNDLEA transaction bundle is paid out and its payout files are available (carries bundleId)

PAYMENT and REFUND come from the payment-request subscription; BUNDLE comes from the transactions subscription. Branch on the notificationType field in the body, and log and acknowledge any value you don't recognize rather than failing.

See Tikkie webhook payloads in action. Inspect and replay sample Tikkie webhook payloads in the Hookdeck Console — no account or setup required.

Setting up Tikkie webhooks

Tikkie has no dashboard screen for notifications, so you subscribe through the API. Every call needs two headers: API-Key, the consumer key of your app on the ABN AMRO developer portal, and X-App-Token, a UUID app token. In production you create the app token in the Tikkie Business Portal; in the sandbox you create one with POST /sandboxapps, sending only the API-Key.

To receive payments and refunds, subscribe with an app token that has payment request permission:

curl -X POST https://api-sandbox.abnamro.com/v2/tikkie/paymentrequestssubscription \
  -H "API-Key: $TIKKIE_API_KEY" \
  -H "X-App-Token: $TIKKIE_APP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://your-app.com/webhooks/tikkie"}'

A 201 response returns a subscriptionId. Store it: every notification includes it, and your handler checks against it. For BUNDLE notifications, make the same call to /transactionssubscription with an app token that has transaction bundle permission. That subscription returns its own subscriptionId, so if both point at the same URL, keep both ids. To unsubscribe, send DELETE to the same endpoint (it returns 204). Tikkie rejects bad URLs with URL_MISSING, URL_INVALID, or URL_DISALLOWED.

There is no signing secret to save, because Tikkie doesn't issue one.

For local development, use the Hookdeck CLI: hookdeck listen 3000 tikkie --path /webhooks/tikkie gives you a public HTTPS URL that forwards to your local server, plus a web UI for inspecting and replaying deliveries, with no account required. Register the generated URL as the url in your sandbox subscription. To test without waiting for a real payment, POST a sample notification body to your endpoint.

Securing Tikkie webhooks

Tikkie notifications are unsigned. The Tikkie API v2.3 OpenAPI spec defines a JSON body and a 2XX acknowledgement for the callbacks, and nothing else: no signature header, secret, HMAC, timestamp, or IP allowlist. For the same reason, Hookdeck's Tikkie source type accepts POST only and has no verification step. Any HMAC verifier you write for Tikkie is checking inputs that don't exist.

What you can do is reduce the risk with two checks. The first is weak: compare the subscriptionId against the id(s) Tikkie returned when you subscribed. It filters out noise and misdirected traffic, but the id appears in every delivery and isn't a secret-grade credential. The second is the real control: treat the notification as a trigger and re-fetch the record from the Tikkie API with your own credentials. Made-up tokens return a 404, and the amount and status you act on come from Tikkie's API response, never from the notification body. Because nothing is signed, you don't need the raw body, so the standard JSON parser is fine:

const crypto = require("crypto");

const allowedIds = (process.env.TIKKIE_SUBSCRIPTION_ID || "")
  .split(",").map((s) => s.trim()).filter(Boolean);

function checkSubscriptionId(id) {
  return allowedIds.some((allowed) => {
    const a = Buffer.from(id), b = Buffer.from(allowed);
    return a.length === b.length && crypto.timingSafeEqual(a, b);
  });
}

function recordPath(n) {
  const e = encodeURIComponent;
  switch (n.notificationType) {
    case "PAYMENT":
      return `/paymentrequests/${e(n.paymentRequestToken)}/payments/${e(n.paymentToken)}`;
    case "REFUND":
      return `/paymentrequests/${e(n.paymentRequestToken)}/payments/${e(n.paymentToken)}/refunds/${e(n.refundToken)}`;
    case "BUNDLE":
      return `/transactionbundles/${e(n.bundleId)}`;
  }
}

app.post("/webhooks/tikkie", express.json(), async (req, res) => {
  const n = req.body;

  // 1. Weak check: subscriptionId must match the id returned at subscribe time
  if (typeof n?.subscriptionId !== "string" || !checkSubscriptionId(n.subscriptionId)) {
    return res.status(403).send("Unknown subscriptionId");
  }

  const path = recordPath(n);
  if (!path) {
    return res.status(200).send("OK"); // unknown notificationType: log and acknowledge
  }

  // 2. Real control: re-fetch the authoritative record from the Tikkie API
  const r = await fetch(`${process.env.TIKKIE_API_BASE_URL}${path}`, {
    headers: {
      "API-Key": process.env.TIKKIE_API_KEY,
      "X-App-Token": process.env.TIKKIE_APP_TOKEN,
    },
  });
  if (r.status === 404) {
    return res.status(403).send("Notification could not be confirmed");
  }
  if (!r.ok) {
    return res.status(502).send("Could not confirm notification"); // Tikkie retries
  }
  const record = await r.json();

  switch (n.notificationType) {
    case "PAYMENT":
      // Mark the order paid using record.amountInCents
      break;
    case "REFUND":
      // Record the refund (record.status is PENDING or PAID)
      break;
    case "BUNDLE":
      // Download and reconcile the bundled payout
      break;
  }

  res.status(200).send("OK");
});

You can also register a hard-to-guess callback URL with a random path segment, such as /webhooks/tikkie/3f9c..., and keep it out of your logs. Tikkie doesn't document query-string secrets, so put the random part in the path.

Note the environment: a sandbox notification re-fetched against the production base URL (or the reverse) returns 404, which this handler treats as forged, so match TIKKIE_API_BASE_URL to the subscription.

Make Tikkie notifications production-ready. Hookdeck Event Gateway gives you a dedicated ingestion URL, deduplicates retried notifications, and durably queues every event with replay for anything that fails.

Tikkie webhook limitations and pain points

Notifications are unsigned

The Problem: There is no way to prove a Tikkie notification came from Tikkie. Anyone who learns your callback URL can POST a well-formed PAYMENT body to it.

Why It Happens: The Tikkie API v2 spec defines no signature, secret, timestamp, or source-IP allowlist for notification callbacks.

Workarounds:

  • Reject notifications whose subscriptionId doesn't match the id(s) returned when you subscribed.
  • Re-fetch every payment, refund, or bundle from the Tikkie API and treat a 404 as forged.
  • Register a callback URL with a random path segment and treat it as confidential.

How Hookdeck Can Help: Hookdeck does not verify Tikkie notifications, because there is no signature to check. What it gives you is a dedicated ingestion URL, a filter on the subscriptionId body field that drops mismatched requests before they reach your handler, and a full log of every request's headers and body so you can see exactly what arrived.

The notification carries only tokens

The Problem: A PAYMENT notification tells you a payment happened on a request, not how much was paid or by whom. Every notification costs at least one extra API call before you can act on it.

Why It Happens: Tikkie keeps the notification body to subscriptionId, notificationType, and tokens. The amount (amountInCents), counterparty, and refund status live on the Payment and Refund objects you fetch with GET /paymentrequests/{paymentRequestToken}/payments/{paymentToken} and its /refunds/{refundToken} sub-resource.

Workarounds:

  • Build the fetch-back into the handler from day one; never fulfill from the notification body.
  • Return a 5xx when the Tikkie API call fails for reasons other than a 404, so the notification is retried.
  • Keep the API credentials and base URL in environment variables per environment.

How Hookdeck Can Help: When your fetch-back fails and your handler returns a 5xx, Hookdeck retries delivery to your endpoint on your own retry schedule, rather than leaving recovery to Tikkie's few attempts. Every failed attempt is logged with the response your handler returned.

Best-effort delivery with three attempts

The Problem: Tikkie describes its delivery as best effort, with a maximum of three attempts. If your endpoint is down for longer than those attempts take, the notification is gone, and the spec itself says not to rely solely on notifications.

Why It Happens: Tikkie's retry mechanism is capped, and the spec documents no timeout and no ordering guarantee.

Workarounds:

  • Acknowledge quickly and defer heavy work so a slow handler doesn't turn into a failed attempt.
  • Run a reconciliation job that polls your open payment requests through the Tikkie API's GET endpoints.
  • Alert on handler error rates so you can fix failures inside the retry window.

How Hookdeck Can Help: Once a notification reaches Hookdeck, it is durably queued ahead of your endpoint, with automatic retries, Issues that alert you when deliveries fail, and replay for any event, so an outage in your own infrastructure no longer means a lost payment. Keep the reconciliation poll as well: Hookdeck can't recover a notification Tikkie never sent.

One subscription per type

The Problem: Tikkie allows one active payment-request subscription and one active transactions subscription. Subscribing again overwrites the existing URL, so pointing a test endpoint at the same app silently redirects production notifications.

Why It Happens: Tikkie stores a single URL per subscription type and treats a repeat POST as a replacement.

Workarounds:

  • Use the sandbox (with its own app token from POST /sandboxapps) for development and testing.
  • Record which URL each subscription points at, and re-check it after any deploy that touches subscription code.
  • Point both subscriptions at one endpoint and branch on notificationType, keeping both subscriptionId values in your allowlist.

How Hookdeck Can Help: Subscribe once with a Hookdeck source URL and leave it there. Fan out from that one URL to multiple destinations (production, staging, a CLI session), and use filters to route PAYMENT and REFUND to one service and BUNDLE to another, without touching the Tikkie subscription again.

Best practices

Treat the notification as a trigger

Never read amounts or status from the notification. Re-fetch the Payment, Refund, or transaction bundle from the Tikkie API with your API-Key and X-App-Token, and act only on what the API returns. This is both your data source and your strongest defense against forged requests.

Check the subscriptionId and keep the URL private

Store each subscriptionId returned at subscribe time and reject anything that doesn't match, comparing with a timing-safe function. If both subscriptions share a URL, allow both ids. Add a random path segment to the callback URL and don't log it in full.

Acknowledge fast, process asynchronously

The spec documents no timeout, so don't depend on a generous one. The re-fetch is a single GET; anything heavier, such as fulfillment, emails, or an accounting sync, belongs in a queue or background job. See why to process webhooks asynchronously.

Make handlers idempotent

Retries mean the same notification can arrive more than once. Deduplicate on paymentToken for PAYMENT, refundToken for REFUND, and bundleId for BUNDLE, so a retried payment never marks an order paid twice. See our guide to webhook idempotency.

Reconcile with polling

Tikkie's own spec encourages you to implement a GET alongside notifications. Run a scheduled job that checks open payment requests through the API, so a notification that never arrived still ends with the order marked paid.

Conclusion

Tikkie notifications cover the three moments that matter for a payment-request integration (a payment, a refund, and a transaction bundle payout) in a flat body keyed by notificationType. The body carries no signature, amount, or status, so a production integration checks the subscriptionId, re-fetches the record from the Tikkie API, and acts only on what the API returns.

Delivery is best effort with a maximum of three attempts, so an endpoint that acknowledges fast, deduplicates on tokens, and reconciles by polling is what keeps payments from going missing. Hookdeck Event Gateway puts a durable queue with retries, deduplication, filtering, and replay in front of your endpoint, so a bad deploy on your side doesn't cost you a payment notification.

Get started with Hookdeck for free and handle Tikkie webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.