# Guide to Tikkie Webhooks: Features and Best Practices

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

| Feature | Details |
| --- | --- |
| Configuration | API only: `POST /paymentrequestssubscription` (payments and refunds) and `POST /transactionssubscription` (transaction bundles) |
| Authentication | None. No signature header, shared secret, HMAC, timestamp header, or published IP allowlist |
| Envelope | Flat JSON: `subscriptionId`, `notificationType`, plus the tokens for that type |
| Event type | `notificationType` body field (`PAYMENT`, `REFUND`, `BUNDLE`); there is no event-type header |
| Delivery | HTTP POST with `Content-Type: application/json`; any `2XX` acknowledges |
| Retries | Best effort, with a maximum of three attempts |
| Subscriptions | One active subscription per type; a repeat POST overwrites the existing one, URL included |
| API credentials | `API-Key` (ABN AMRO developer portal consumer key) and `X-App-Token` (Tikkie app token) on every call |
| Environments | Production `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:

| Event | Fires when |
| --- | --- |
| `PAYMENT` | A payment is made on one of your payment requests (carries `paymentRequestToken` and `paymentToken`) |
| `REFUND` | A refund on a payment is executed (adds `refundToken`) |
| `BUNDLE` | A 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](https://console.hookdeck.com) — 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:

```bash
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](/docs/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:

```javascript
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](/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](/docs/filters) 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](/docs/retries), 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](/docs/retries), [Issues](/docs/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](/docs/sources) URL and leave it there. Fan out from that one URL to multiple destinations (production, staging, a CLI session), and use [filters](/docs/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](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### 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](/webhooks/guides/implement-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](/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](https://dashboard.hookdeck.com/signup) for free and handle Tikkie webhooks reliably in minutes.