# Adyen payment received or refused notifications on your phone

> Get a push notification with the amount, method and your reference every time an Adyen payment is authorised, and a High-priority one when a payment is refused.

- Company: Adyen (https://www.justpush.io/recipes/adyen)
- Event: `AUTHORISATION` (AUTHORISATION)
- Tags: Payments & billing, Failed payments & disputes, Payments
- Install: https://studio.justpush.io/recipes/adyen/authorisation
- Web page: https://www.justpush.io/recipes/adyen/authorisation

## 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 AUTHORISATION 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. `AUTHORISATION` is one of the Standard webhook's default events, so there's nothing extra to select.
6. Refused payments are pushed too, at High priority. To only hear about received payments, use `your webhook URL (shown in Studio after install)?refused=0` as the webhook URL instead.
7. To see the shopper's email in the push, enable **Shopper email** under **Additional settings** on the webhook. Without it the push shows the amount, payment method and your merchant reference.
8. To test, edit the webhook and select **Test configuration**, then choose `AUTHORISATION`. Adyen sends a handful of test events (successful and failed, in EUR and GBP), so expect a few pushes marked **(test mode)**. Adyen only needs a 2xx response; the old `[accepted]` response body isn't required.
9. 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 → AUTHORISATION
// Fires when a payment is authorised (success "true") or refused (success "false").
// Docs: https://docs.adyen.com/api-explorer/Webhooks/1/post/AUTHORISATION
//
// Needs a Standard webhook (Customer Area > Developers > Webhooks) with Method set to JSON,
// pointing at the integration's endpoint URL. Add ?refused=0 to skip refused payments.

// 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,
}

// Friendlier names for common Adyen payment method codes. Unknown codes are shown as sent.
const METHODS = {
    visa: "Visa",
    mc: "Mastercard",
    amex: "Amex",
    maestro: "Maestro",
    cup: "UnionPay",
    jcb: "JCB",
    diners: "Diners",
    discover: "Discover",
    cartebancaire: "Cartes Bancaires",
    bcmc: "Bancontact",
    ideal: "iDEAL",
    paypal: "PayPal",
    klarna: "Klarna",
    applepay: "Apple Pay",
    googlepay: "Google Pay",
    paywithgoogle: "Google Pay",
    sepadirectdebit: "SEPA Direct Debit",
    ach: "ACH",
    twint: "TWINT",
    blik: "BLIK",
    swish: "Swish",
    mobilepay: "MobilePay",
    vipps: "Vipps",
    trustly: "Trustly",
}

// 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")
}

// "visa" → "Visa", "klarna_paynow" → "Klarna", unknown codes as sent.
function methodName(code) {
    const raw = text(code)
    if (!raw) return null
    const key = raw.toLowerCase()
    return pick(METHODS, key, null) ?? pick(METHODS, key.split("_")[0], null) ?? raw
}

// Last four card digits: additionalData.cardSummary, or the reason of a successful card
// authorisation, which Adyen formats as "authCode:last4:expiry" (e.g. "874574:1935:11/2012").
function lastFour(item, success) {
    const summary = text(item.additionalData?.cardSummary)
    if (summary && /^\d{4}$/.test(summary)) return summary
    const match = success ? /^[^:]*:(\d{4}):\d{1,2}\/\d{2,4}$/.exec(text(item.reason) ?? "") : null
    return match ? match[1] : null
}

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 AUTHORISATION items, so other Adyen events on this integration stay quiet.
    const matches = items.filter((item) => item.eventCode === "AUTHORISATION")
    if (!matches.length) {
        console.log(`Ignoring Adyen "${items[0]?.eventCode ?? "unknown"}" webhook`)
        return null
    }

    const item = matches[0]
    const outcome = String(item.success).toLowerCase()
    if (outcome !== "true" && outcome !== "false") {
        console.log(`Skipping AUTHORISATION with success "${item.success}"`)
        return null
    }

    const success = outcome === "true"
    // ?refused=0 on the webhook URL mutes refused payments.
    const showRefused = !["0", "false", "no"].includes(String(request.query?.refused ?? "").toLowerCase())
    if (!success && !showRefused) {
        console.log("Skipping refused payment (?refused=0)")
        return null
    }

    const method = methodName(item.paymentMethod)
    const last4 = lastFour(item, success)
    const who = text(item.additionalData?.shopperEmail)
    const reason = success ? null : text(item.reason)
    const ref = text(item.merchantReference)
    const more = matches.length > 1 ? ` (+${matches.length - 1} more)` : ""
    const test = body.live === "false" || body.live === false ? " (test mode)" : ""

    const message =
        (money(item.amount) ?? "A payment") +
        (method ? ` via ${method}` : "") +
        (last4 ? ` ••${last4}` : "") +
        (who ? ` from ${who}` : "") +
        (success ? "" : " refused") +
        (reason && reason.toLowerCase() !== "refused" ? `: ${reason}` : "") +
        (ref ? ` — ${ref}` : "") +
        more +
        test

    return {
        title: success ? "💰 Payment received" : "❌ Payment refused",
        message,
        topic: "Adyen",
        // Received: Normal, money in is good news. Refused: High, a sale didn't go through.
        priority: success ? 0 : 1,
        ...(success ? { sound: "cashregister" } : {}),
    }
}
```

## Adyen AUTHORISATION webhook payload (sample)

```json
{
  "live": "true",
  "notificationItems": [
    {
      "NotificationRequestItem": {
        "amount": {
          "value": 4995,
          "currency": "EUR"
        },
        "reason": "874574:1935:03/2030",
        "success": "true",
        "eventCode": "AUTHORISATION",
        "eventDate": "2026-09-28T11:29:48+02:00",
        "operations": [
          "CANCEL",
          "CAPTURE",
          "REFUND"
        ],
        "pspReference": "QFQTPCQ8HXSKGK82",
        "paymentMethod": "visa",
        "additionalData": {
          "shopperEmail": "jane@example.com",
          "hmacSignature": "REDACTED_BASE64_SIGNATURE="
        },
        "merchantReference": "ORDER-12345",
        "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.
