> ## Documentation Index
> Fetch the complete documentation index at: https://docs.switchbord.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Payment receipt link via URL button

> Configure an external payment or booking system to send WhatsApp receipt links through Switchbord.

This guide shows the Option A receipt pattern: the WhatsApp template body contains receipt details and a dynamic URL button opens the hosted receipt page.

Use this pattern when your external system already hosts receipt pages and you want a small, expected Utility message in WhatsApp.

## Before you start

You need:

* A connected WhatsApp channel in Switchbord.
* A Switchbord API key for the workspace if calling `POST /api/v1/template-sends` directly.
* An approved Utility template with a URL button, for example `payment_receipt_link`.
* A public HTTPS receipt page such as `https://pay.example.com/receipts/R-1001`.

<Warning>
  Do not use this flow for promotional messages. Utility templates should be tied to customer-expected events such as payment, instalment, booking, refund, or document availability.
</Warning>

## Template shape

A typical receipt-link template contains body variables and a URL button:

```text theme={null}
Ciao {{1}}, abbiamo ricevuto il pagamento {{2}} per la ricevuta {{3}}.
```

```text theme={null}
Button URL: https://pay.example.com/receipts/{{1}}
```

The button variable is the dynamic suffix. If the base URL is `https://pay.example.com/receipts/`, send only `R-1001` as the variable.

## Direct API send

```bash theme={null}
curl -X POST https://api.switchbord.ai/api/v1/template-sends \
  -H "Authorization: Bearer $SWITCHBORD_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: payment-receipt-R-1001" \
  -d '{
    "templateName": "payment_receipt_link",
    "templateLocale": "it",
    "recipient": {
      "phoneE164": "+393331234567",
      "externalId": "traveller-123",
      "name": "Giulia Rossi"
    },
    "variables": {
      "body": {
        "1": "Giulia",
        "2": "€ 240,00",
        "3": "R-1001"
      },
      "buttons": {
        "url": [
          { "index": 0, "suffix": "R-1001" }
        ]
      }
    },
    "metadata": {
      "source": "booking-system",
      "eventType": "payment.receipt.created"
    }
  }'
```

Expected response:

```json theme={null}
{
  "data": {
    "contactId": "contact_123",
    "conversationId": "conversation_123",
    "messageId": "message_123",
    "clientMessageId": "payment-receipt-R-1001",
    "status": "queued",
    "reused": false,
    "idempotent": true
  }
}
```

## Inbound webhook builder

If the external system can call a webhook but cannot store a Switchbord API key, configure a workspace inbound webhook instead:

1. Open **Settings → Integrations → Webhooks & API**.
2. Create a new inbound webhook.
3. Select the approved `payment_receipt_link` Utility template.
4. Map the required fields:

| Template field      | Source mode  | Example source        |
| ------------------- | ------------ | --------------------- |
| Recipient phone     | Payload path | `customer.phone`      |
| External ID         | Payload path | `receipt.id`          |
| Body `{{1}}`        | Payload path | `customer.first_name` |
| Body `{{2}}`        | Payload path | `receipt.amount`      |
| Body `{{3}}`        | Payload path | `receipt.number`      |
| URL button variable | Payload path | `receipt.number`      |

1. Paste a sample payload and run the dry-run preview.
2. Create the webhook. The endpoint stays disabled/dry-run until a future run-log verified promotion flow enables live execution.

Sample payload:

```json theme={null}
{
  "customer": {
    "phone": "+393331234567",
    "first_name": "Giulia"
  },
  "receipt": {
    "id": "payment-receipt-R-1001",
    "number": "R-1001",
    "amount": "€ 240,00",
    "link": "https://pay.example.com/receipts/R-1001"
  }
}
```

## Signing inbound webhook requests

Webhook requests use timestamped HMAC signatures. The secret is shown only once when the webhook is created.

```ts theme={null}
import { createHash, createHmac } from "node:crypto";

const rawBody = JSON.stringify(payload);
const timestamp = Math.floor(Date.now() / 1000);
const secretHash = createHash("sha256").update(webhookSecret).digest("hex");
const digest = createHmac("sha256", secretHash)
  .update(`${timestamp}.${rawBody}`)
  .digest("hex");

await fetch(webhookUrl, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-Switchbord-Signature": `t=${timestamp},v1=${digest}`,
    "Idempotency-Key": payload.receipt.id
  },
  body: rawBody
});
```

Never send the raw webhook secret as a header, query parameter, or JSON field.

## Rollout checklist

* Run the browser dry-run preview and verify the mapped request.
* Do not expect signed endpoint run logs yet for Settings-created configs; they remain disabled until live promotion ships.
* Confirm the template is approved Utility in the same workspace.
* Use stable idempotency keys from the upstream event id.
* Keep receipt URLs HTTPS-only and avoid embedding long-lived private tokens.

## Troubleshooting

| Symptom                             | Check                                                                                                     |
| ----------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `404 webhook_not_found_or_disabled` | Settings-created configs are disabled today; use browser preview or the direct API until promotion ships. |
| Future `401 missing_timestamp`      | `X-Switchbord-Signature` must include `t=<unix seconds>`.                                                 |
| Future `401 expired_timestamp`      | Sender clock differs by more than five minutes.                                                           |
| Future `401 invalid_signature`      | Sign the exact raw request body, not a re-serialized copy.                                                |
| `400 template_not_approved_utility` | Select an approved Utility template in the same workspace.                                                |
| `400 missing_button_url_variable`   | Map the URL button suffix and ensure it resolves to a non-empty string.                                   |
| Duplicate sends                     | Use a stable `Idempotency-Key` for each receipt event.                                                    |
