# Quickbase Webhooks

Quickbase table webhooks fire when a record is **added, modified or deleted**. You write the whole
request: endpoint URL, HTTP method (POST, GET, PUT, PATCH or DELETE; POST is the default), message
format (XML, JSON or RAW), up to 10 message headers and the message body. Webhooks are **not
signed**. The only way to authenticate the sender is a secret header you add yourself in the
webhook's **Message header** list, which your endpoint then checks.

## When to Use This Skill

* How do I receive Quickbase webhooks?
* How do I authenticate a Quickbase webhook if it has no signature?
* How do I handle record added, modified and deleted webhooks?
* Why does my Quickbase webhook endpoint return 401?
* Why does my endpoint get a PUT, PATCH, GET or DELETE instead of a POST?

## Verification (core)

The header name is your choice, so the examples read it from `QUICKBASE_WEBHOOK_HEADER` (default
`Authorization`) and the expected value from `QUICKBASE_WEBHOOK_SECRET`: the exact, whole header
value, including any `Bearer ` prefix you typed. Look the header up case-insensitively, compare the
**whole** value (never strip or parse `Bearer `), and compare in constant time.

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

function verifyQuickbaseRequest(headers, secret = process.env.QUICKBASE_WEBHOOK_SECRET) {
  if (!secret) return false; // never accept when unconfigured
  const name = (process.env.QUICKBASE_WEBHOOK_HEADER || 'Authorization').toLowerCase();
  const received = headers[name]; // Node lowercases header names
  if (typeof received !== 'string') return false;
  const a = Buffer.from(received, 'latin1'); // headers arrive as latin1 text
  const b = Buffer.from(secret, 'utf8');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

```

```python
import hmac

def verify_quickbase_request(headers, secret, header_name="Authorization") -> bool:
    received = headers.get(header_name)  # Starlette headers are case-insensitive
    if not secret or received is None:
        return False
    # compare_digest needs bytes: str raises TypeError on non-ASCII
    return hmac.compare_digest(received.encode("latin-1"), secret.encode("utf-8"))

```

* Missing header, wrong value or unset secret: `401`.
* A static shared credential authenticates the **sender, not the body**, and gives **no replay
  protection** (no signature, no timestamp). Use HTTPS, and rotate the credential if it leaks.
* Webhooks can be viewed and copied by every admin of the Quickbase app, so the secret is visible
  to them. Use a value dedicated to this webhook.
* Read the body raw (`express.raw({ type: '*/*' })`) so any content type works, and parse it only
  after the credential check.
* Accept `POST`, `PUT`, `PATCH`, `GET` and `DELETE` on the route, and handle an empty body.

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

## Hookdeck Source

Hookdeck's Quickbase source (type `QUICKBASE`) performs the same check: header name plus the exact
whole value. It pre-selects `POST` without managing the method, so if your webhook uses PUT, PATCH,
GET or DELETE, also allow that method on the source.

## Event Types

Quickbase adds **no event field** to the request, and there are no event name strings. The three
triggers you can select under **When** are a record being added, modified or deleted. What the
request contains is the message body template you wrote. The examples dispatch on an
**illustrative** `event` field that you add yourself in the template (one webhook per trigger, with
the value typed as a literal). It is not a Quickbase field.

| Trigger (When) | Illustrative `event` value in your template |
| --- | --- |
| A record is added | `record_added` |
| A record is modified | `record_modified` |
| A record is deleted | `record_deleted` |

Illustrative JSON template (field references chosen from the dropdown go where the values are):

```json
{ "event": "record_modified", "record_id": 42, "table": "Projects" }

```

## Environment Variables

```bash
QUICKBASE_WEBHOOK_HEADER=Authorization        # name of the header you added (default: Authorization)
QUICKBASE_WEBHOOK_SECRET=Bearer your-secret   # exact, whole header value

```

## Local Development

Quickbase can't reach `localhost`. Tunnel with the Hookdeck CLI (no account required):

```bash
npx hookdeck-cli listen 3000 quickbase --path /webhooks/quickbase

```

Use the printed URL as the **Endpoint URL** in the Quickbase webhook (it must be `https://`).

## Resources

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/quickbase-webhooks/references/overview.md) - What Quickbase webhooks are, triggers, message format
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/quickbase-webhooks/references/setup.md) - Configure the webhook and secret header
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/quickbase-webhooks/references/verification.md) - Header check details, gotchas, debugging 401s
* [examples/](https://github.com/hookdeck/webhook-skills/tree/main/skills/quickbase-webhooks/examples/) - Runnable Express, Next.js and FastAPI handlers with tests