> This page is for version 2024-10-01 (default).
> For other versions, use one of these documentation indexes:
> - 2024-10-01 (default): https://docs.getkard.com/2024-10-01/llms.txt
> - Legacy: https://docs.getkard.com/legacy/llms.txt

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.getkard.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.getkard.com/_mcp/server.

# Notifications

## Overview

Kard sends near real-time webhooks about important events related to transactions in your rewards program. They come in two kinds, and the difference determines what you do with them.

**Reward events (`earnedRewardApproved` and `earnedRewardSettled`) are built to be passed as push notifications straight through to your users.** Each carries suggested notification text and an attribution link, so a matched transaction can become a push notification without any additional lookups.

**`earnedRewardRejected` is not intended to be shown to your users.** It reports that a transaction which initially matched has been rejected and will not result in a reward — a signal for your system to act on internally, not something to forward as a push notification. That's why the payload carries a `reason` instead of an attribution link.

Each event type is delivered to the webhook URL you designate for it. You subscribe to events yourself via the [Create Subscriptions](/2024-10-01/api/notifications/subscriptions/create) endpoint. Every delivery is HMAC-signed so you can confirm it came from Kard.

## How it works

1. **Subscribe.** Call [Create Subscriptions](/2024-10-01/api/notifications/subscriptions/create) with the event type you want and the URL it should be delivered to.
2. **Receive.** When a qualifying event occurs, Kard POSTs the notification to that URL with a `notify-signature` header.
3. **Verify.** Recompute the HMAC over the body with your webhook key and compare it to the header. Reject anything that doesn't match.
4. **Respond.** Return a `2xx`. Use the payload's `id` as an idempotency key so a redelivery never notifies a user twice.

## Event types

| Event                  | Sent when                                                                               | Best practices                                                             |
| ---------------------- | --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| `earnedRewardApproved` | An approved transaction qualifies for a reward.                                         | **Notify the user.** Carries message text and an attribution link.         |
| `earnedRewardSettled`  | A settled transaction qualifies for a reward.                                           | **Notify the user.** Carries message text and an attribution link.         |
| `earnedRewardRejected` | A transaction that initially matched is later rejected and does not result in a reward. | **Reconcile internally.** No attribution link; carries a `reason` instead. |

Full payload schemas for each event live in the [Notifications Webhook API reference](/2024-10-01/api/notifications/notification-webhook).

## What's in a payload

All three events share a core: a unique `id` on the envelope, plus `message`, `transactionId`, and `transactionAmountInCents` in `attributes` (`transactionTimestamp` is optional). Use `id` as an idempotency key so a redelivery never double-notifies a user or double-reverses a reward, and `transactionId` to tie the event back to the transaction you submitted.

What differs is what surrounds that core — and, critically, whether `message` is fit to show a user.

### Reward events: `earnedRewardApproved`, `earnedRewardSettled`

These are the payloads designed for pass-through to your users.

* **`message`**: user-ready copy. Send it as a push notification as-is.
* **`attributionUrl`**: tracks your user's interactions with the notification. See the [Attributions guide](/2024-10-01/api/attributions) for how to use it.
* **`name`**: the merchant name.
* **Offer context**: `categoryName`, `userReward`, `purchaseChannel`, `surveyUrl`, and `assets` (merchant images whose URLs are signed for attribution tracking, to be loaded as-is). `earnedRewardSettled` also carries `commissionEarned`.

`attributes.message` can be served directly as a push notification:

![earnedRewardApproved push notification](/_fern-img/c7653a0e62e1bee8f87303c2f2ac20e5a26ebe13d17c3fe0af8c765e61e7b99d.webp)

**`earnedRewardApproved`**: the reward is pending, so the message names no amount.

![earnedRewardSettled push notification](/_fern-img/3a349f3554ee2ba6dfc44977e890d75a4b09ed1c795430259164222495de5759.webp)

**`earnedRewardSettled`**: the reward is final, so the message states the amount.

### Rejections: `earnedRewardRejected`

* **`reason`**: why the transaction was rejected. Values below.
* **`message`**: every rejection carries the same string — `Your transaction did not result in a reward.`
* **No `attributionUrl`.**

| `reason`                       | Meaning                                                                                                           |
| ------------------------------ | ----------------------------------------------------------------------------------------------------------------- |
| `AGGREGATOR_CARD_OVERLAP`      | The card is already linked to another user at Kard, so the transaction was rejected at match time.                |
| `MAX_REDEMPTION_LIMIT_REACHED` | The user has already redeemed this offer as many times as it allows, so the transaction earned no further reward. |
| `SETTLEMENT_REJECTED`          | The transaction was approved at match time but rejected at settlement.                                            |
| `USER_NOT_ENROLLED`            | The user is not enrolled in cardlinked rewards.                                                                   |
| `USER_NOT_IN_AUDIENCE_SEGMENT` | The user is not part of the audience segment eligible for the offer.                                              |

The `reason` values are listed above. Kard may add new values over time, so handle any unrecognized value as a generic rejection rather than failing the request.

## Verifying the signature

Notifications are outbound POSTs to a URL you provide, authenticated with an HMAC signature rather than a shared credential on your side.

You'll be issued a webhook key. Kard uses it to compute an HMAC of the webhook body and sends the result in the `notify-signature` header. To validate a delivery, compute the same HMAC yourself — your key, the request body, SHA-256 — and compare it to the header value. Header names are matched case-insensitively.

### Code recipe: verify, then ingest

A minimal Node.js service that verifies the signature in middleware and processes the payload in the route.

**`auth.js`** — signature verification middleware

```javascript
const { createHmac } = require("crypto");
const secretKey = issuer_webhook_key; //provided in postman_environment.json

const verifyToken = (req, res, next) => {
    // grab HMAC signature from Notify-signature header of request
    const token = req.get("notify-signature");

    if (!token) {
        return res.status(403).send("A token is required for authentication");
    }

    try {
        // cast webhook as string
        const stringRequest = JSON.stringify(req.body);

        // hash using sha256, webhook key, and webhook body as string
        const hash = createHmac('sha256', secretKey)
            .update(stringRequest)
            .digest('base64')

        // verified request
        if (token === hash){
            return next();
        }

        // unverified request
        return res.status(401).send("Invalid Token");
    } catch (err) {
        return res.status(400).send("Bad Request");
    }
};

module.exports = verifyToken;
```

**`index.js`** — the POST endpoint

```javascript
const express = require('express');
const auth = require('./auth');

const port = 3000;
const app = express();

app.use(express.json());

app.post('/notifications-webhook', auth, (req, res) => {
    try {
        // insert code that processes the webhook
        console.log('notification webhook: ', req.body);
    }
    catch (err) {
        res.send(err);
    }
    res.status(200).send('Thanks Kard!');
});

app.listen(port, () => {
    console.log(`Example app listening on port ${port}`);
});
```

## Testing your integration (Sandbox environment only)

> **Sandbox environment only.** The trigger endpoint described here exists only in Kard's **test (sandbox)** environment and is not available in production.

Once your endpoint verifies signatures, you can confirm it handles earned-reward webhooks end to end without waiting on a real matched transaction. The [**Simulate Test Notification**](/2024-10-01/api/notifications/notifications/trigger) endpoint sends a simulated `earnedRewardApproved` or `earnedRewardSettled` webhook to the URL you subscribed. You supply the `userId` and `transactionId` to include in the simulated transaction; offer information is sourced from an existing sandbox offer, so the payload matches the shape of a production notification and is HMAC-signed like any other delivery.

**Prerequisite:** an enabled subscription for the event you want to trigger (see [Create Subscriptions](/2024-10-01/api/notifications/subscriptions/create)). If there isn't one, the call returns `409`.

```bash
curl -X POST "https://<test-api-host>/v2/issuers/{organizationId}/notifications" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
        "data": [
          {
            "type": "simulateTestTransaction",
            "attributes": {
              "userId": "issuer-user-123",
              "transactionId": "network-txn-9f8e7d6c",
              "eventName": "earnedRewardSettled"
            }
          }
        ]
      }'
```

A successful call returns `202` with the generated `eventId`. The notification is delivered to your webhook and can be listed and replayed like any other. Each call generates a new `eventId`, so you can trigger as many test notifications as you need (subject to a per-issuer rate limit).