# Square refund issued notifications on your phone

> Get a low-priority push notification when a Square payment is refunded, from the Dashboard, Point of Sale or the Refunds API, with the amount and reason.

- Company: Square (https://www.justpush.io/recipes/square)
- Event: `refund.created` (refund.created)
- Tags: Payments & billing, Refunds
- Install: https://studio.justpush.io/recipes/square/refund-created
- Web page: https://www.justpush.io/recipes/square/refund-created

## 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 the Square Developer Console (developer.squareup.com/apps), open your application (create a free one if you have none) and switch to Production.
3. Under Webhooks, choose Subscriptions and click Add subscription.
4. Paste your webhook URL (shown in Studio after install) as the notification URL, tick the refund.created event, and save.
5. Tick **refund.created** only. Square also sends **refund.updated** each time the refund's status changes (for example from pending to completed); the recipe ignores those so you get one push per refund.
6. Sandbox deliveries are marked **(sandbox)**.
7. Square signs every notification with an `x-square-hmacsha256-signature` header (an HMAC-SHA256 of your notification URL and the body, keyed with the subscription's signature key), but JustPush doesn't check that signature yet, so keep your endpoint URL private.

## 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
// Square → refund.created
// Fires once when a payment is refunded, from the Square Dashboard, Point of Sale,
// Terminal or the Refunds API.
// Docs: https://developer.squareup.com/docs/refunds-api/webhooks

// Where the money goes back to, in plain words.
const DESTINATIONS = {
    CARD: "card",
    BANK_ACCOUNT: "bank account",
    WALLET: "digital wallet",
    BUY_NOW_PAY_LATER: "buy now, pay later",
    CASH: "cash",
    SQUARE_ACCOUNT: "Square account",
}

// Header names can arrive in any case.
function header(request, name) {
    const headers = request.headers || {}
    const key = Object.keys(headers).find((k) => k.toLowerCase() === name)
    const value = key ? headers[key] : null
    return Array.isArray(value) ? value[0] : value
}

// Table lookup that ignores inherited names such as "toString".
function pick(table, key, fallback) {
    return typeof key === "string" && Object.prototype.hasOwnProperty.call(table, key) ? table[key] : fallback
}

// Square Money is { amount, currency } with amount in the currency's smallest unit
// (cents for USD/EUR/GBP, whole yen for JPY). Returns null when there's no amount.
function money(value) {
    const amount = value?.amount
    const currency = value?.currency

    if (typeof amount !== "number" || !Number.isFinite(amount) || typeof currency !== "string" || !currency) return null

    try {
        const format = new Intl.NumberFormat("en-GB", { style: "currency", currency })
        return format.format(amount / 10 ** format.resolvedOptions().maximumFractionDigits)
    } catch {
        return null
    }
}

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

    // Guard: only refund.created. refund.updated follows as the refund completes, and
    // is skipped so each refund gives one push.
    if (body.type !== "refund.created" || !refund || typeof refund !== "object") {
        console.log(`Ignoring Square event "${typeof body.type === "string" ? body.type : "unknown"}"`)
        return null
    }

    // A refund is normally PENDING or COMPLETED when created; REJECTED/FAILED means it didn't go through.
    const failed = refund.status === "REJECTED" || refund.status === "FAILED"
    const amount = money(refund.amount_money)
    const to = pick(DESTINATIONS, refund.destination_type, null)
    const why = typeof refund.reason === "string" && refund.reason.trim() ? refund.reason.trim().slice(0, 100) : null
    const sandbox = String(header(request, "square-environment") ?? "").toLowerCase() === "sandbox" ? " (sandbox)" : ""

    return {
        title: failed ? "⚠️ Refund failed" : "↩️ Refund issued",
        message:
            (failed
                ? `${amount ?? "A refund"} couldn't be refunded`
                : amount ? `${amount} refunded` : "A payment was refunded") +
            (to ? ` to ${to}` : "") +
            (why ? ` — ${why}` : "") +
            sandbox,
        topic: "Square",
        // Low for a normal refund (you or a teammate most likely issued it yourself),
        // High when Square rejected it, because the customer is still waiting for their money.
        priority: failed ? 1 : -1,
    }
}
```

## Square refund.created webhook payload (sample)

```json
{
  "data": {
    "id": "KkAkhdMsgzn59SM8A89WgKwekxLZY_ptNBVqHYxt5gAdfcobBe4u1AZsXhoz06KTtuq9Ls24P",
    "type": "refund",
    "object": {
      "refund": {
        "id": "KkAkhdMsgzn59SM8A89WgKwekxLZY_ptNBVqHYxt5gAdfcobBe4u1AZsXhoz06KTtuq9Ls24P",
        "reason": "Item arrived damaged",
        "status": "PENDING",
        "version": 7,
        "order_id": "haOyDuHiqtAXMk0d8pDKXpL7Jg4F",
        "created_at": "2026-09-28T16:27:41.836Z",
        "payment_id": "KkAkhdMsgzn59SM8A89WgKwekxLZY",
        "updated_at": "2026-09-28T16:27:41.846Z",
        "location_id": "NAQ1FHV6ZJ8YV",
        "amount_money": {
          "amount": 1500,
          "currency": "USD"
        }
      }
    }
  },
  "type": "refund.created",
  "event_id": "bc316346-6691-4243-88ed-6d651a0d0c47",
  "created_at": "2026-09-28T16:27:41.852Z",
  "merchant_id": "6SSW7HV8K2ST5"
}
```

## 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 Square 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.
