# Adyen chargeback notifications on your phone

> Get a Highest-priority push notification when a shopper disputes an Adyen payment, with the reason and the defense deadline, plus pushes for requests for information, second chargebacks and reversals.

- Company: Adyen (https://www.justpush.io/recipes/adyen)
- Event: `CHARGEBACK` (the dispute events (NOTIFICATION_OF_CHARGEBACK, CHARGEBACK, REQUEST_FOR_INFORMATION, SECOND_CHARGEBACK, CHARGEBACK_REVERSED))
- Tags: Payments & billing, Failed payments & disputes
- Install: https://studio.justpush.io/recipes/adyen/chargeback
- Web page: https://www.justpush.io/recipes/adyen/chargeback

## Setup

1. Click Install in Studio and sign in. The recipe is added to your account and you get a personal webhook URL.
2. In your Adyen Customer Area, go to Developers > Webhooks, select Create new webhook and add a Standard webhook.
3. Paste your webhook URL (shown in Studio after install) as the URL and set Method to JSON.
4. Under Events settings, check that the dispute events (NOTIFICATION_OF_CHARGEBACK, CHARGEBACK, REQUEST_FOR_INFORMATION, SECOND_CHARGEBACK, CHARGEBACK_REVERSED) is selected, then select Save configuration.
5. Use a **Standard** webhook and set **Method** to **JSON**. The HTTP POST and SOAP methods send the data in a different format that this recipe can't read. Under **Events settings**, make sure all the dispute events are selected.
6. The recipe pushes for `NOTIFICATION_OF_CHARGEBACK` (a dispute was opened), `CHARGEBACK` (the money was deducted), `REQUEST_FOR_INFORMATION`, `SECOND_CHARGEBACK` and `CHARGEBACK_REVERSED`. One dispute usually sends a `NOTIFICATION_OF_CHARGEBACK` first and a `CHARGEBACK` later, so you get a push at each stage. Disputes Adyen defends automatically come in at Normal priority. Other dispute events, such as `NOTIFICATION_OF_FRAUD` or pre-arbitration, are ignored.
7. Adyen's **Test configuration** button can only send `CHARGEBACK_REVERSED` of these events. To see a real dispute push, make a test card payment with a special `holderName` (see Adyen's "Test a chargeback scenario" guide) and capture it; those pushes are marked **(test mode)**. Adyen only needs a 2xx response; the old `[accepted]` response body isn't required.
8. Adyen signs each event with an HMAC signature in `additionalData.hmacSignature`, but JustPush doesn't check it yet, so keep your endpoint URL private. You can leave the HMAC key and basic authentication settings empty.

## Code

Studio calls `handleRequest(request)` with the incoming webhook (`{ method, headers, body, query, raw }`) and sends the message object it returns. Returning `null` sends nothing.

```js
// Adyen → dispute events (NOTIFICATION_OF_CHARGEBACK, CHARGEBACK, REQUEST_FOR_INFORMATION,
// SECOND_CHARGEBACK, CHARGEBACK_REVERSED)
// Fires when a shopper's bank opens a dispute, asks for information, charges a payment back
// (again), or when a defended chargeback is returned to you.
// Docs: https://docs.adyen.com/risk-management/disputes-api/dispute-notifications/
//
// Needs a Standard webhook (Customer Area > Developers > Webhooks) with Method set to JSON and
// the dispute events selected, pointing at the integration's endpoint URL.

// Adyen amounts are in minor units, and Adyen sets the decimals per currency. Everything not
// listed here has two. https://docs.adyen.com/development-resources/currency-codes
const DECIMALS = {
    CVE: 0, DJF: 0, GNF: 0, IDR: 0, JPY: 0, KMF: 0, KRW: 0, PYG: 0,
    RWF: 0, UGX: 0, VND: 0, VUV: 0, XAF: 0, XOF: 0, XPF: 0,
    BHD: 3, IQD: 3, JOD: 3, KWD: 3, LYD: 3, OMR: 3, TND: 3,
}

// Each dispute event, how to word it, and whether it opens a defense window.
const EVENTS = {
    NOTIFICATION_OF_CHARGEBACK: { title: "⚖️ Chargeback incoming", verb: "disputed", defend: true },
    CHARGEBACK: { title: "⚖️ Chargeback", verb: "charged back", defend: true },
    REQUEST_FOR_INFORMATION: { title: "📄 Request for information", verb: "queried by the shopper's bank", priority: 1 },
    SECOND_CHARGEBACK: { title: "⚖️ Second chargeback", verb: "charged back again", priority: 1 },
    CHARGEBACK_REVERSED: { title: "✅ Chargeback reversed", verb: "returned to your account", priority: -1 },
}

// Own-property lookup, so keys like "toString" fall back instead of hitting Object.prototype.
function pick(table, key, fallback) {
    return Object.prototype.hasOwnProperty.call(table, key) ? table[key] : fallback
}

// A trimmed string, or null. Adyen sometimes sends the literal string "null".
function text(value) {
    if (typeof value !== "string") return null
    const trimmed = value.trim()
    return trimmed && trimmed.toLowerCase() !== "null" ? trimmed : null
}

// { value: 4995, currency: "EUR" } → "€49.95". Returns null when either part is missing.
function money(amount) {
    const raw = amount?.value
    const minor = typeof raw === "string" && /^-?\d+$/.test(raw) ? Number(raw) : raw
    const code = text(amount?.currency)?.toUpperCase()

    if (typeof minor !== "number" || !Number.isFinite(minor) || !code) return null

    const value = minor / 10 ** pick(DECIMALS, code, 2)

    try {
        return new Intl.NumberFormat("en-GB", { style: "currency", currency: code }).format(value)
    } catch {
        return `${value} ${code}`
    }
}

// The NotificationRequestItem objects in the body. JSON webhooks carry one, but it's an array.
function notificationItems(body) {
    const list = Array.isArray(body?.notificationItems) ? body.notificationItems : []
    return list.map((wrapper) => wrapper?.NotificationRequestItem).filter((item) => item && typeof item === "object")
}

// "2026-10-14T16:05:30+02:00" → "14 Oct". Returns null for anything unparseable.
function day(iso) {
    const date = new Date(text(iso) ?? "")
    if (Number.isNaN(date.getTime())) return null
    return date.toLocaleDateString("en-GB", { day: "numeric", month: "short", timeZone: "UTC" })
}

function handleRequest(request) {
    const body = request.body && typeof request.body === "object" ? request.body : {}
    const items = notificationItems(body)

    // A custom test notification from Adyen; confirm the connection quietly.
    const ping = items.find((item) => item.eventCode === "NOTIFICATIONTEST")
    if (ping) {
        return {
            title: "🔔 Adyen connected",
            message: `Webhook for ${text(ping.merchantAccountCode) ?? "your merchant account"} is working`,
            topic: "Adyen",
            priority: -1,
        }
    }

    // Guard: only the dispute events above, so other Adyen events on this integration stay quiet.
    const matches = items.filter((item) => pick(EVENTS, item.eventCode, null))
    if (!matches.length) {
        console.log(`Ignoring Adyen "${items[0]?.eventCode ?? "unknown"}" webhook`)
        return null
    }

    const item = matches[0]
    const event = EVENTS[item.eventCode]
    const extra = item.additionalData && typeof item.additionalData === "object" ? item.additionalData : {}
    const amount = money(item.amount)
    const reason = text(item.reason)
    const ref = text(item.merchantReference)
    const autoDefended = String(extra.autoDefended).toLowerCase() === "true"
    const undefendable = String(extra.defendable).toLowerCase() === "false"
    const dueBy = day(extra.defensePeriodEndsAt)
    const more = matches.length > 1 ? ` (+${matches.length - 1} more)` : ""
    const test = body.live === "false" || body.live === false ? " (test mode)" : ""

    let next = ""
    let priority = event.priority
    if (event.defend) {
        if (autoDefended) {
            next = ". Adyen defended it automatically."
            priority = 0 // Normal — nothing to do unless you want to add evidence
        } else if (undefendable) {
            next = ". It can't be defended."
            priority = 1 // High — money is (or will be) gone, but there's nothing to submit
        } else {
            next = dueBy ? `. Respond by ${dueBy}.` : ". Respond in your Customer Area."
            priority = 2 // Highest — the defense clock is ticking
        }
    } else if (item.eventCode === "REQUEST_FOR_INFORMATION" && dueBy) {
        // High — answering the RFI can prevent a chargeback.
        next = `. Respond by ${dueBy}.`
    }
    // SECOND_CHARGEBACK is High: the defense was rejected and the money is gone again.
    // CHARGEBACK_REVERSED is Low: good news, the funds are back (though not always final).

    return {
        title: event.title,
        message:
            (amount ? `${amount} ${event.verb}` : `A payment was ${event.verb}`) +
            (reason ? ` — ${reason}` : "") +
            (ref ? ` (${ref})` : "") +
            more +
            test +
            next,
        topic: "Adyen",
        priority,
    }
}
```

## Adyen CHARGEBACK webhook payload (sample)

```json
{
  "live": "true",
  "notificationItems": [
    {
      "NotificationRequestItem": {
        "amount": {
          "value": 4995,
          "currency": "EUR"
        },
        "reason": "Other Fraud-Card Absent Environment",
        "success": "true",
        "eventCode": "NOTIFICATION_OF_CHARGEBACK",
        "eventDate": "2026-09-29T16:05:30+02:00",
        "pspReference": "D7XK2M4PQ9RT5W82",
        "paymentMethod": "visa",
        "additionalData": {
          "defendable": "true",
          "autoDefended": "false",
          "disputeStatus": "Undefended",
          "hmacSignature": "REDACTED_BASE64_SIGNATURE=",
          "defensePeriodEndsAt": "2026-10-14T16:05:30+02:00",
          "chargebackReasonCode": "10.4",
          "chargebackSchemeCode": "visa",
          "modificationMerchantReferences": ""
        },
        "merchantReference": "ORDER-12345",
        "originalReference": "QFQTPCQ8HXSKGK82",
        "merchantAccountCode": "YourCompanyECOM"
      }
    }
  ]
}
```

## FAQ

### Does this work on iPhone and Android?

Yes. Install the JustPush app from the App Store or Google Play and sign in. Every phone signed in to your account gets the notification.

### Do I need to write code?

No. Install the recipe in Studio, paste your webhook URL into Adyen and you are done. The code is there if you want to change the text, the sound or the buttons.

### Can I change what the notification says?

Yes. After install the recipe's code is yours. Edit it in Studio and test it against the sample payload before you save.

### What does it cost?

JustPush is free for 30 days. After that it's $19.99 a year, or $39.99 once. There is no extra charge for recipes.
