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

# Notification Channels

> Route approval, payment execution, and Conto Pay lifecycle events to email, Slack, Telegram, WhatsApp, and custom webhooks

# Notification Channels

Notification channels send approval requests and webhook lifecycle events to external platforms.
Approvers can approve or reject directly from their email inbox, Slack workspace, Telegram chat, or
WhatsApp conversation. Webhook channels can also receive machine-readable payment execution and
Conto Pay request events for agents and back-office systems.

## How It Works

```
Payment triggers approval workflow
  → Conto generates secure, one-time action tokens
    → Notifications sent to all configured channels
      → Approver clicks Approve/Reject
        → Token is validated and consumed
          → Same approval logic as the dashboard
```

Every external decision uses the same `submitApprovalDecision()` path. Audit logs, atomic counters, webhook delivery, and race-condition safety all apply regardless of channel.

## Supported Channels

| Channel  | Delivery                       | How Approvers Act                 |
| -------- | ------------------------------ | --------------------------------- |
| Email    | Rich HTML via Resend           | Click Approve/Reject button links |
| Slack    | Block Kit message with buttons | Click interactive action buttons  |
| Telegram | Message with inline keyboard   | Tap inline keyboard buttons       |
| WhatsApp | Interactive button message     | Tap reply buttons                 |
| Webhook  | JSON POST with tokens          | POST tokens back to Conto API     |

## Setting Up Channels

Go to **Settings** > **Channels** in the Conto dashboard, or use the REST API.

<Steps>
  <Step title="Choose a channel type">
    Click **Add Channel** and select the platform: Email, Slack, Telegram, WhatsApp, or Webhook.
  </Step>

  <Step title="Configure credentials">
    Each channel type requires different configuration:

    | Channel  | Required Config                                       |
    | -------- | ----------------------------------------------------- |
    | Email    | Comma-separated recipient email addresses             |
    | Slack    | Bot token (`xoxb-...`) and channel ID                 |
    | Telegram | Bot token, chat ID, and optional webhook secret token |
    | WhatsApp | Phone number ID, access token, recipient numbers      |
    | Webhook  | HTTPS URL and a signing secret (`secret` in config)   |

    Webhook targets must resolve to public addresses. Conto rejects loopback, private, link-local, and other internal-only destinations. In production, webhook channels must use HTTPS.
  </Step>

  <Step title="Select event types">
    Choose which events trigger notifications:

    | Event                        | When It Fires                                   | Notes                          |
    | ---------------------------- | ----------------------------------------------- | ------------------------------ |
    | `approval.requested`         | New payment needs approval                      | Available on all channel types |
    | `approval.decided`           | Someone approved or rejected                    | Available on all channel types |
    | `approval.escalated`         | Request escalated after timeout                 | Available on all channel types |
    | `approval.expired`           | Request expired without resolution              | Available on all channel types |
    | `payment.executed`           | Payment is submitted onchain                    | `WEBHOOK` channels only        |
    | `conto_pay.request.received` | A hosted Conto Pay request is sent to the payer | `WEBHOOK` channels only        |
    | `conto_pay.request.paid`     | A hosted Conto Pay request is paid              | `WEBHOOK` channels only        |
    | `conto_pay.request.rejected` | A hosted Conto Pay request is rejected          | `WEBHOOK` channels only        |
    | `conto_pay.request.expired`  | A hosted Conto Pay request expires              | `WEBHOOK` channels only        |
  </Step>

  <Step title="Test the channel">
    Click the test button to send a sample notification and verify your configuration.
  </Step>
</Steps>

## Channel Configuration Details

<AccordionGroup>
  <Accordion title="Email">
    Uses Resend to deliver rich HTML emails. Each eligible approver receives their own email with unique Approve and Reject buttons.

    **Config fields:**

    * `recipients` - List of email addresses. Only addresses matching eligible approvers receive actionable emails.

    Clicking a button opens a browser, validates the token, submits the decision, and shows a confirmation page.
  </Accordion>

  <Accordion title="Slack">
    Requires a Slack app with the `chat:write` bot scope. Messages use Block Kit with payment details and interactive Approve/Reject buttons.

    **Config fields:**

    * `botToken` - Your Slack app's bot token (`xoxb-...`)
    * `channelId` - The Slack channel ID to post messages to

    **Setup:**

    1. Create a Slack app at [api.slack.com/apps](https://api.slack.com/apps)
    2. Add the `chat:write` bot scope
    3. Install the app to your workspace
    4. Set the interactivity request URL to `https://conto.finance/api/webhooks/slack`
    5. Set `SLACK_SIGNING_SECRET` in your Conto environment

    When an approver clicks a button, Slack sends the interaction to Conto. The message is updated to show the result.
  </Accordion>

  <Accordion title="Telegram">
    Uses the Telegram Bot API to send messages with inline keyboard buttons.

    **Config fields:**

    * `botToken` - Your Telegram bot token from [@BotFather](https://t.me/BotFather)
    * `chatId` - The chat or group ID to send messages to
    * `secretToken` - Optional webhook secret. When set, Conto requires Telegram to send the matching `x-telegram-bot-api-secret-token` header on every callback.

    **Setup:**

    1. Create a bot via @BotFather
    2. Set the webhook URL: `https://api.telegram.org/bot{token}/setWebhook?url=https://conto.finance/api/webhooks/telegram`
    3. If you configure `secretToken`, include Telegram's `secret_token` option when calling `setWebhook`
    4. Add the bot to your group chat

    Conto only accepts interactive callbacks for the configured `chatId`. If
    `secretToken` is set, Telegram callbacks must also include the matching
    secret header before approval tokens are processed.

    When an approver taps a button, the message updates to show the result.
  </Accordion>

  <Accordion title="WhatsApp">
    Uses the WhatsApp Cloud API to send interactive button messages.

    **Config fields:**

    * `phoneNumberId` - Your WhatsApp Business phone number ID
    * `accessToken` - Permanent access token from Meta
    * `recipientNumbers` - Phone numbers in international format (e.g., `+1234567890`)

    **Setup:**

    1. Register at [developers.facebook.com](https://developers.facebook.com)
    2. Create a WhatsApp Business app
    3. Set the webhook URL to `https://conto.finance/api/webhooks/whatsapp`
    4. Set `WHATSAPP_APP_SECRET` and `WHATSAPP_VERIFY_TOKEN` in your Conto environment
    5. Subscribe to the `messages` webhook field
  </Accordion>

  <Accordion title="Webhook">
    Sends a signed JSON payload to any HTTPS endpoint. Use this for custom integrations that need to review and submit approval decisions.

    **Config fields:**

    * `url` - Your HTTPS endpoint URL
    * `secret` - Signing secret for HMAC verification (required; `signingSecret` and `webhookSecret` are accepted aliases)

    Every delivery is signed: the `X-Conto-Signature` header carries an HMAC-SHA256 of
    `${timestamp}.${rawBody}` computed with your channel secret. Verify it before trusting the
    payload.

    **Validation rules:**

    * The destination must resolve to a public IP address
    * Private, loopback, link-local, and `.internal` / `.local` hosts are rejected
    * In production, only `https://` webhook targets are accepted

    **Payload format:**

    ```json theme={null}
    {
      "event": "approval.requested",
      "approvalRequestId": "clxyz123...",
      "paymentDetails": {
        "amount": 500,
        "currency": "USDC",
        "recipientAddress": "0x5678...",
        "agentName": "Treasury Agent"
      },
      "tokens": {
        "approve": "base64url-token",
        "reject": "base64url-token"
      },
      "actionUrl": "https://conto.finance/api/approvals/action",
      "timestamp": "2026-04-06T12:00:00Z"
    }
    ```

    To submit a decision, POST the token back to the `actionUrl`:

    ```bash theme={null}
    curl -X POST https://conto.finance/api/approvals/action \
      -H "X-Action-Token: base64url-token" \
      -H "Content-Type: application/json" \
      -d '{"comment": "Approved via external review system"}'
    ```
  </Accordion>
</AccordionGroup>

## Conto Pay Lifecycle Webhooks

Webhook channels can subscribe to hosted Conto Pay request lifecycle events:
`conto_pay.request.received`, `conto_pay.request.paid`, `conto_pay.request.rejected`, and
`conto_pay.request.expired`.

The `received` event is delivered to the payer organization. Terminal outcomes (`paid`,
`rejected`, and `expired`) are delivered to both the payer and payee organizations when they have
matching active webhook channels.

Each Conto Pay webhook payload includes absolute hosted links for the request review page and the
payer action page. These match the copyable links shown in the Conto Pay activity inbox and hosted
request review screens, so external systems and human operators can refer to the same URL.

```json theme={null}
{
  "event": "conto_pay.request.paid",
  "request": {
    "id": "ipr_...",
    "status": "PAID",
    "amount": 250,
    "currency": "pathUSD",
    "purpose": "Invoice INV-100",
    "invoiceId": "INV-100",
    "dueDate": "2026-06-30T00:00:00.000Z",
    "expiresAt": "2026-07-01T00:00:00.000Z",
    "createdAt": "2026-06-14T12:00:00.000Z",
    "updatedAt": "2026-06-14T12:05:00.000Z",
    "direction": "incoming"
  },
  "payer": {
    "agentId": "agent_payer",
    "agentName": "Buyer AP Agent",
    "agentSlug": "accounts-payable",
    "organizationId": "org_buyer",
    "organizationName": "Buyer Inc",
    "organizationSlug": "buyer-inc",
    "handle": "@buyer-inc/accounts-payable"
  },
  "payee": {
    "agentId": "agent_payee",
    "agentName": "Vendor AR Agent",
    "agentSlug": "accounts-receivable",
    "organizationId": "org_vendor",
    "organizationName": "Vendor LLC",
    "organizationSlug": "vendor-llc",
    "handle": "@vendor-llc/accounts-receivable"
  },
  "links": {
    "requestUrl": "https://conto.finance/pay-requests/ipr_...",
    "payerActionUrl": "https://conto.finance/conto-pay?request=ipr_..."
  },
  "recipientOrganizationId": "org_buyer",
  "transaction": {
    "id": "tx_...",
    "txHash": "0xabc123",
    "status": "CONFIRMED"
  },
  "timestamp": "2026-06-14T12:05:00.000Z"
}
```

Use `request.direction` to render the event from the receiving organization's point of view:
`incoming` means the organization is the payer, while `outgoing` means the organization created the
request and is waiting on the payer.

## REST API

Manage channels programmatically:

```bash theme={null}
# List channels
curl https://conto.finance/api/notification-channels \
  -H "Authorization: Bearer $API_KEY"

# Create a channel
curl -X POST https://conto.finance/api/notification-channels \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "channelType": "SLACK",
    "name": "Treasury Channel",
    "config": { "botToken": "xoxb-...", "channelId": "C012345" },
    "eventTypes": ["approval.requested", "approval.decided", "payment.executed"]
  }'

# Test a channel
curl -X POST https://conto.finance/api/notification-channels/{id}/test \
  -H "Authorization: Bearer $API_KEY"

# Delete a channel
curl -X DELETE https://conto.finance/api/notification-channels/{id} \
  -H "Authorization: Bearer $API_KEY"
```

## Action Token Security

Action tokens are the core security mechanism for external approvals.

* **32 bytes of cryptographic randomness**, base64url-encoded
* **Protected at rest**. Plaintext tokens are not retained after issuance
* **One-time use**. Tokens cannot be reused after a decision is recorded
* **Time-limited**. Tokens expire when the approval request expires (default 24 hours)
* **Per-user, per-action**. Each approver gets separate Approve and Reject tokens
* **Full audit trail**. Every decision records the channel, token ID, IP address, and user agent

## Environment Variables

| Variable                | Required For | Description                                        |
| ----------------------- | ------------ | -------------------------------------------------- |
| `RESEND_API_KEY`        | Email        | Resend API key for sending emails                  |
| `SLACK_SIGNING_SECRET`  | Slack        | Slack app signing secret for webhook verification  |
| `WHATSAPP_APP_SECRET`   | WhatsApp     | Meta app secret for webhook signature verification |
| `WHATSAPP_VERIFY_TOKEN` | WhatsApp     | Token for Meta webhook verification challenge      |

## Webhook Payload Validation

Inbound webhook payloads from Slack, Telegram, and WhatsApp are validated
against typed Zod schemas before processing. Malformed payloads are rejected
with a `400` response and never reach handler logic. Signature verification
(HMAC) still runs first for all three providers regardless of payload shape.

## Delta Verification Channel (pilot)

Delta is a guided pilot that routes payment approvals through an external invoice-verification
service. It is not a self-serve channel type: Conto activates it for your organization during
onboarding as a standard `WEBHOOK` channel with additional Delta configuration. For access,
onboarding inputs, and activation details, see [Delta Verification Setup](/guides/delta-setup); to
validate the live flow end to end, see [Delta Smoke Test](/guides/delta-smoke-test).

Two things differ from a standard webhook channel:

**1. Enriched payloads.** When `approval.requested` fires for a Delta-enabled channel, the
outbound payload carries the full payment context in `paymentDetails` (`paymentRequestId`,
`senderAddress`, `chainId`, `category`, `memo`, `executionMode`, `walletId`, `context`,
`metadata`, and timestamps, in addition to the standard fields) plus invoice context from the
typed `PaymentRequest` invoice record when present, with a fallback to legacy `context.invoice`
data for older requests. It also includes the counterparty record and a pair of pre-minted
single-use action tokens so the pilot verification service can call back immediately. Abbreviated:

```json theme={null}
{
  "event": "approval.requested",
  "approvalRequestId": "ar_...",
  "paymentDetails": {
    /* full payment context, plus: */
    "counterparty": {
      "id": "cp_...",
      "name": "Vendor LLC",
      "address": "0x...",
      "trustLevel": "TRUSTED",
      "approvalStatus": "APPROVED"
    }
  },
  "invoice": {
    "vendorId": "...",
    "vendorAddress": "0x...",
    "invoiceId": "INV-1042",
    "invoiceHash": "...",
    "invoiceSourceUrl": "https://...",
    "invoicePayload": {
      /* raw invoice */
    },
    "expectedAmount": "12345",
    "currency": "USDC",
    "dueDate": "2026-06-01"
  },
  "actionToken": {
    "approveToken": "<base64url, single-use>",
    "rejectToken": "<base64url, single-use>",
    "expiresAt": "...",
    "callbackUrl": "https://app.example/api/approvals/action"
  },
  "dashboardUrl": "https://app.example/dashboard?approvalRequestId=ar_...",
  "timestamp": "..."
}
```

If the same Delta channel also subscribes to `payment.executed`, Conto sends an execution webhook
after the payment is submitted onchain, with the same invoice enrichment plus a `transaction`
block (`transactionId`, `transactionHash`, `explorerUrl`, `executedAt`), so the verifier can mark
its invoice row `PAID` without polling. Non-Delta `WEBHOOK` channels keep the standard payload
shape; this enrichment only appears for organizations that Conto has enabled for Delta. HMAC
signing is identical to the standard webhook channel
(`X-Conto-Signature: sha256(${X-Conto-Timestamp}.${rawBody})`).

**2. Signed decision callback.** For production verification services, prefer the signed callback
route to record the decision and (optionally) attach a proof reference:

```bash theme={null}
POST /api/internal/approval-requests/{approvalRequestId}/decide
X-Conto-Timestamp: 2026-05-26T18:00:00Z
X-Conto-Signature: <hex HMAC-SHA256 of `${timestamp}.${rawBody}` using config.secret>
Content-Type: application/json

{
  "decision": "APPROVED",
  "comment": "Verified against template invoice_payment_guard",
  "verification": {
    "proofType": "delta_signed",
    "proofRef": "https://delta.example/proofs/abc",
    "templateId": "...",
    "intentUuid": "...",
    "evidenceHash": "...",
    "reason": "OK"
  }
}
```

The `actionToken.callbackUrl` in the webhook payload remains the backwards-compatible demo path:
POST the `approveToken` or `rejectToken` to `/api/approvals/action` with the same optional
`verification {}` block. Both routes persist the same verification data, and proof material
surfaces on the **Delta** tab in the dashboard once your organization has Delta enabled.

## Next Steps

<CardGroup cols={2}>
  <Card title="Approval Workflows" icon="check-double" href="/policies/advanced">
    Configure multi-approval workflows with escalation and sequential approvals
  </Card>

  <Card title="Securing Agents" icon="shield" href="/guides/securing-agents">
    Set up spending limits and approval thresholds for your agents
  </Card>
</CardGroup>
