# Square payment received notifications on your phone

> Get a push notification with the amount, tip and card when a Square payment completes, from your online checkout, Point of Sale or Terminal.

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

## 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 payment.updated event, and save.
5. Subscribe to **payment.updated** only. Square fires it several times per payment (when it's authorised, completed, when processing fees are added, when it's refunded). The recipe pushes only for a completed payment and skips the follow-up updates that carry processing fees or a refund, so you normally get one push per sale.
6. Square can still send an extra early update for the same payment (for example when it links a customer), which can occasionally give a second push. If you notice Point of Sale or Terminal sales that never push, also tick **payment.created**: the recipe then pushes for payments that are created already completed, at the risk of an extra push for some sales.
7. Payments on invoices also push here. If you use the Invoice paid recipe as well, you'll get both.
8. Sandbox and Production have separate webhook subscriptions, so add this one in Production for real sales. Sandbox deliveries are marked **(sandbox)**.
9. 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 → payment.updated (payment completed)
// Fires when a payment reaches COMPLETED: a card sale, a captured authorisation, a paid
// invoice or a cleared ACH transfer, from the Square API, Point of Sale or Terminal.
// Docs: https://developer.squareup.com/docs/payments-api/webhooks

// Square doesn't send an event-name header; the event type is in the body's "type".
const EVENTS = ["payment.updated", "payment.created"]

// Friendlier names for Square's card brands and payment sources.
const BRANDS = {
    VISA: "Visa",
    MASTERCARD: "Mastercard",
    AMERICAN_EXPRESS: "American Express",
    DISCOVER: "Discover",
    DISCOVER_DINERS: "Diners Club",
    JCB: "JCB",
    CHINA_UNIONPAY: "UnionPay",
    SQUARE_GIFT_CARD: "Square gift card",
    INTERAC: "Interac",
    EFTPOS: "eftpos",
    FELICA: "FeliCa",
    EBT: "EBT",
}

const WALLETS = {
    CASH_APP: "Cash App Pay",
    PAYPAY: "PayPay",
    ALIPAY: "Alipay",
    RAKUTEN_PAY: "Rakuten Pay",
    AU_PAY: "au PAY",
    D_BARAI: "d払い",
    MERPAY: "Merpay",
    WECHAT_PAY: "WeChat Pay",
    LIGHTNING: "Bitcoin Lightning",
}

const BNPL = {
    AFTERPAY: "Afterpay",
    CLEARPAY: "Clearpay",
}

const SOURCES = {
    CASH: "cash",
    BANK_ACCOUNT: "bank transfer",
    SQUARE_ACCOUNT: "Square account",
    EXTERNAL: "an external method",
}

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

// Only link to real http(s) URLs.
function link(url, cta) {
    return typeof url === "string" && /^https?:\/\//.test(url) ? [{ cta, url }] : []
}

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

// "via Visa •••• 1111", "via cash", "via Cash App Pay" …
function method(payment) {
    if (payment.source_type === "CARD") {
        const card = payment.card_details?.card ?? {}
        return pick(BRANDS, card.card_brand, "card") + (card.last_4 ? ` •••• ${card.last_4}` : "")
    }
    if (payment.source_type === "WALLET") return pick(WALLETS, payment.wallet_details?.brand, "a digital wallet")
    if (payment.source_type === "BUY_NOW_PAY_LATER") return pick(BNPL, payment.buy_now_pay_later_details?.brand, "buy now, pay later")
    return pick(SOURCES, payment.source_type, null)
}

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

    // Guard: only payment events that carry the payment object.
    if (!EVENTS.includes(body.type) || !payment || typeof payment !== "object") {
        console.log(`Ignoring Square event "${typeof body.type === "string" ? body.type : "unknown"}"`)
        return null
    }

    // Square sends payment.updated for every change (authorised, completed, fees added,
    // refunded …). Only a completed payment is worth a push.
    if (payment.status !== "COMPLETED") {
        console.log(`Skipping payment with status "${payment.status ?? "unknown"}"`)
        return null
    }

    // Follow-up updates for the same payment: Square adds processing fees in a separate
    // payment.updated after completion, and refunds update refunded_money.
    if (Array.isArray(payment.processing_fee) && payment.processing_fee.length > 0) {
        console.log("Skipping follow-up update (processing fees added)")
        return null
    }
    if (payment.refunded_money?.amount > 0) {
        console.log("Skipping follow-up update (payment was refunded)")
        return null
    }

    const total = money(payment.total_money) ?? money(payment.amount_money) ?? "A payment"
    const tip = payment.tip_money?.amount > 0 ? money(payment.tip_money) : null
    const via = method(payment)
    const note = typeof payment.note === "string" && payment.note.trim() ? payment.note.trim().slice(0, 100) : null
    const sandbox = String(header(request, "square-environment") ?? "").toLowerCase() === "sandbox" ? " (sandbox)" : ""

    return {
        title: "💰 Payment received",
        message:
            total +
            (tip ? ` (incl. ${tip} tip)` : "") +
            (via ? ` via ${via}` : "") +
            (payment.buyer_email_address ? ` from ${payment.buyer_email_address}` : "") +
            (note ? ` — ${note}` : "") +
            sandbox,
        topic: "Square",
        priority: 0, // Normal — money in is good news, not an emergency
        sound: "cashregister",
        buttons: link(payment.receipt_url, "View receipt"),
    }
}
```

## Square payment.updated webhook payload (sample)

```json
{
  "data": {
    "id": "hYy9pRFVxpDsO1FB05SunFWUe9JZY",
    "type": "payment",
    "object": {
      "payment": {
        "id": "hYy9pRFVxpDsO1FB05SunFWUe9JZY",
        "note": "Catering order #1042",
        "status": "COMPLETED",
        "order_id": "03O3USaPaAaFnI6kkwB1JxGgBsUZY",
        "tip_money": {
          "amount": 500,
          "currency": "USD"
        },
        "created_at": "2026-09-28T14:16:51.086Z",
        "updated_at": "2026-09-28T14:19:00.831Z",
        "location_id": "S8GWD5R9QB376",
        "receipt_url": "https://squareup.com/receipt/preview/hYy9pRFVxpDsO1FB05SunFWUe9JZY",
        "source_type": "CARD",
        "total_money": {
          "amount": 4700,
          "currency": "USD"
        },
        "amount_money": {
          "amount": 4200,
          "currency": "USD"
        },
        "card_details": {
          "card": {
            "bin": "540988",
            "last_4": "9029",
            "exp_year": 2029,
            "card_type": "CREDIT",
            "exp_month": 11,
            "card_brand": "MASTERCARD",
            "prepaid_type": "NOT_PREPAID"
          },
          "status": "CAPTURED",
          "avs_status": "AVS_ACCEPTED",
          "cvv_status": "CVV_ACCEPTED",
          "entry_method": "KEYED",
          "card_payment_timeline": {
            "captured_at": "2026-09-28T14:19:00.832Z",
            "authorized_at": "2026-09-28T14:16:51.198Z"
          },
          "statement_description": "SQ *EXAMPLE CAFE"
        },
        "version_token": "bhC3b8qKJvNDdxqKzXaeDsAjS1oMFuAKxGgT32HbE6S6o",
        "approved_money": {
          "amount": 4200,
          "currency": "USD"
        },
        "receipt_number": "hYy9",
        "buyer_email_address": "jane@example.com"
      }
    }
  },
  "type": "payment.updated",
  "event_id": "6a8f5f28-54a1-4eb0-a98a-3111513fd4fc",
  "created_at": "2026-09-28T14:19:01.012Z",
  "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.
