# Rutter Webhooks

## When to Use This Skill

* How do I receive Rutter webhooks?
* How do I verify the `X-Rutter-Signature` header?
* Why is my Rutter webhook signature verification failing?
* How do I accept webhooks during a Rutter client secret rotation (`X-Rutter-Signatures`)?
* How do I handle `INITIAL_UPDATE`, `CONNECTION_NEEDS_UPDATE`, `ORDER_CREATED` or `INVOICE_UPDATED` events?

## Verification (core)

Rutter signs the **raw request body** with HMAC-SHA256, keyed with your organization's **client secret** (used as a raw string, not decoded). `X-Rutter-Signature` is `sha256=` + the **base64** digest. `X-Rutter-Signatures` is a comma-separated list of every active signature: one entry normally, two during a client secret rotation (new secret first, expiring secret second). Accept the webhook if any signature you compute from any of your active secrets matches any entry in the list.

```javascript
const crypto = require('crypto');

function verifyRutter(rawBody, signaturesHeader, secrets) {
  if (!signaturesHeader) return false;
  const received = signaturesHeader.split(',').map((s) => s.trim());
  return secrets.some((secret) => {
    const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('base64');
    return received.some((sig) => {
      try {
        return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
      } catch {
        return false; // different lengths
      }
    });
  });
}

```

Python equivalent:

```python
import base64, hashlib, hmac

def verify_rutter(raw_body: bytes, signatures_header: str, secrets: list[str]) -> bool:
    received = [s.strip() for s in (signatures_header or "").split(",") if s.strip()]
    for secret in secrets:
        digest = hmac.new(secret.encode(), raw_body, hashlib.sha256).digest()
        expected = "sha256=" + base64.b64encode(digest).decode()
        if any(hmac.compare_digest(sig.encode(), expected.encode()) for sig in received):
            return True
    return False

```

Rutter does not send a timestamp or webhook ID header, so there is no signed timestamp to check. Use the payload (`type`, `code`, entity `id`) for idempotency. Rutter has no official webhook verification SDK helper, so the examples verify manually.

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/rutter-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/rutter-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/rutter-webhooks/examples/fastapi/).

## Payload and Event Types

Every payload has a `type` (entity group, e.g. `CONNECTION`, `ORDER`) and a `code` (the specific event), plus `access_token` and `connection_id`. Entity events add the entity object under a lowercase key (`order`, `invoice`, ...). Route on `code`.

| `type` | `code` | Description |
| --- | --- | --- |
| `CONNECTION` | `INITIAL_UPDATE` | Initial data download finished; data can now be fetched |
| `CONNECTION` | `CONNECTION_NEEDS_UPDATE` | Merchant unauthorized or auth failed; payload has `update_url` to re-authenticate |
| `CONNECTION` | `CONNECTION_DISABLED` | Rutter disabled access (e.g. store suspended); has `disable_error`, `disable_message` |
| `ORDER` | `ORDER_CREATED` / `ORDER_UPDATED` / `ORDER_DELETED` | Commerce order changes |
| `INVOICE` | `INVOICE_CREATED` / `INVOICE_UPDATED` | Accounting invoice changes |

Other connection codes: `HISTORICAL_UPDATE`, `INCREMENTAL_SYNC_COMPLETED`, `CONNECTION_UPDATED`, `CONNECTION_LINK_ERROR`, `CONNECTION_ERROR`, `CONNECTION_AUTHENTICATED`. See [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/rutter-webhooks/references/overview.md) for the full list.

> Rutter must receive a 2xx within **5 seconds**; failures are retried with exponential backoff for up to 15 minutes. The same payload can be delivered more than once, and a small share of webhooks can be dropped, so handlers must be idempotent and you should also poll the API.

## Environment Variables

```bash
RUTTER_CLIENT_SECRET=your_client_secret            # Rutter dashboard, where you get your API keys
RUTTER_CLIENT_SECRET_PREVIOUS=                      # Optional: expiring secret during rotation

```

## Local Development

```bash
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 rutter --path /webhooks/rutter

```

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/rutter-webhooks/references/overview.md) - Rutter webhook concepts, payloads, event types
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/rutter-webhooks/references/setup.md) - Dashboard configuration and secrets
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/rutter-webhooks/references/verification.md) - Signature verification details and rotation