# Guide to Quickbase Webhooks: Features and Best Practices

Quickbase webhooks send an HTTP request when records in a table change. A new project is added, a ticket's status moves to **Issue**, an order is deleted: each can call a URL with a message you design, so you can keep another system in sync, notify a chat tool, or start a process in your own service the moment the data changes.

This guide covers how Quickbase table webhooks are triggered and built, how to authenticate them with a secret header, the markers that make a message useful to a receiver, and the limits that decide whether a message is sent at all.

## What are Quickbase webhooks?

A Quickbase webhook belongs to one table. Its **When** section picks the trigger: a record added, modified or deleted, in any combination, optionally narrowed to changes in specific fields or to records whose field values match criteria after the change. Its **Then** section is the whole outgoing request: endpoint URL, HTTP method, message format, headers and body.

Because you write the request, Quickbase adds nothing of its own. There's no event field, no fixed schema and no signature. What arrives is the body you composed, with field references and markers replaced by values from the record.

That includes authentication. You add a secret header to the webhook's **Message header** list, and your receiver checks it.

## Quickbase webhook features

| Feature | Details |
| --- | --- |
| Configuration | Table **Settings** > **Workflow & Permissions** > **Webhooks** > **New Webhook**, or `API_Webhooks_Create` |
| Who can create them | App admins and managers |
| Triggers | Record added, modified, deleted, with optional field and value criteria |
| Endpoint | Must begin with `https://` |
| HTTP method | `POST` by default. Also `GET`, `PUT`, `PATCH`, `DELETE` |
| Message format | XML, JSON or RAW |
| Headers | Up to 10 name/value pairs |
| Body | Your template, with field references and markers. Sent as UTF-8 |
| Authentication | A secret header you add. No signature |
| Limits | 10 webhooks per table, 4 MB per message, 50 messages per second per app |
| Error reporting | 30-day error history, and email to the webhook owner |
| Retries and timeout | Not documented |
| Plans | All plans except Quickbase Essential |

## Building the message

Quickbase gives you field references and markers to fill the body. Field references are field names in square brackets, such as `[Status]`, and `[old.status]` gives the value before the change. Markers include:

| Marker | Inserts |
| --- | --- |
| `%action%` | `Add`, `Replace` or `Delete`, depending on what triggered the webhook |
| `%webhookid%` | The webhook's numeric ID |
| `%dbid%`, `%dbName%` | The table's ID and name |
| `%appid%`, `%appName%` | The app's ID and name |
| `%curuser%` | The user who triggered the webhook |
| `%curdate%` | When the webhook was triggered |
| `%reclink%` | A link to the changed record |
| `%RepeatOn%` ... `%RepeatOff%` | Repeated content for each record when one event changes several rows |

A JSON template for your own receiver might be:

```json
{
  "action": "%action%",
  "webhook_id": "%webhookid%",
  "table_id": "%dbid%",
  "record_id": "[Record ID#]",
  "status": "[Status]",
  "previous_status": "[old.Status]",
  "changed_at": "%curdate%"
}

```

Use the field names from your own table. `%action%` and `%webhookid%` together tell your receiver what happened and which webhook sent it. Grid edits, CSV imports, search-and-replace and table merges can change several records in one event; use `%RepeatOn%` and `%RepeatOff%` if you need every row.

Quickbase's documentation gives `Content-Type: application/json` as an example message header, so add it when you send JSON.

> **Inspect Quickbase webhooks as they arrive.** Point a webhook at a [Hookdeck Inspect](https://inspect.hookdeck.com) URL to see the method, headers and rendered body your template produces, with no account or setup required.

## Setting up Quickbase webhooks

Generate a long random value first, for example with `openssl rand -hex 32`.

1. Open the table, select **Settings**, and under **Workflow & Permissions** select **Webhooks**.
2. Select **New Webhook** and give it a **Name** and **Description**.
3. Under **When**, choose added, modified and/or deleted, and any **Additional criteria**.
4. Under **Then**, enter the **Endpoint URL** and choose the **HTTP method** (leave it on `POST` unless you need another).
5. Choose the **Message format**.
6. Under **Message header**, add `Content-Type` if needed, and a secret header such as `Authorization` with the value `Bearer <your random value>`.
7. Write the **Message body**, then select **Save**.

To route through Hookdeck, create a source with the **Quickbase** type and use its URL in step 4. Enter your secret header's name in **API Key Header** and its exact value, including any `Bearer ` prefix, in **API Key**. Hookdeck finds the header case-insensitively and compares the whole value.

The Hookdeck source pre-selects `POST` but doesn't manage the method, because Quickbase lets you choose it. If your webhook uses `PUT`, `PATCH`, `GET` or `DELETE`, add that method to the source's allowed HTTP methods, or Hookdeck won't accept the request.

## Securing Quickbase webhooks

Look up the header you added and compare its whole value with the stored secret, in constant time. Accept the methods your webhook uses, and don't assume there is a body: a `GET` or `DELETE` may have none.

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

const HEADER_NAME = (process.env.QUICKBASE_WEBHOOK_HEADER || 'authorization').toLowerCase();
const EXPECTED = process.env.QUICKBASE_WEBHOOK_SECRET; // e.g. "Bearer 3f9c..."

function safeEqual(received, expected) {
  const a = Buffer.from(received, 'utf8');
  const b = Buffer.from(expected, 'utf8');
  // timingSafeEqual throws on unequal lengths, so check first.
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

const app = express();

// Quickbase lets each webhook choose its method, so accept the ones you configured.
app.all('/webhooks/quickbase', express.raw({ type: '*/*' }), (req, res) => {
  if (!['POST', 'PUT', 'PATCH', 'GET', 'DELETE'].includes(req.method)) {
    return res.sendStatus(405);
  }

  const received = req.get(HEADER_NAME);
  if (!EXPECTED || typeof received !== 'string' || !safeEqual(received, EXPECTED)) {
    return res.sendStatus(401);
  }

  const raw = Buffer.isBuffer(req.body) ? req.body.toString('utf8') : '';
  // Parse raw as JSON or XML to match the webhook's message format, then queue it.
  res.sendStatus(200);
});

```

The same check in Python:

```python
import hmac
import os

HEADER_NAME = os.environ.get("QUICKBASE_WEBHOOK_HEADER", "Authorization")
EXPECTED = os.environ.get("QUICKBASE_WEBHOOK_SECRET", "")

def verify_quickbase_request(headers) -> bool:
    if not EXPECTED:
        return False  # never accept when unconfigured
    received = headers.get(HEADER_NAME)  # most frameworks look headers up case-insensitively
    if not received:
        return False
    # Compare bytes: compare_digest raises TypeError on a non-ASCII str.
    return hmac.compare_digest(received.encode("utf-8"), EXPECTED.encode("utf-8"))

```

A secret header authenticates the sender, not the body. It cannot detect a modified message, and with no signature or timestamp a captured request can be replayed and still pass. Quickbase also warns that webhooks "can be edited or copied by all admins in the app", and any credentials you enter are visible to them. Use a value that exists only for this webhook, keep it long and random, and change it if an admin leaves or it might have leaked.

> **Make Quickbase webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) checks your secret header, accepts each record change straight away, and delivers it to your service with retries, so a short outage doesn't lose an update.

## Quickbase webhook limitations and pain points

### Messages over the limits are not sent

**The Problem:** A bulk import or a busy app silently drops messages your receiver was counting on.

**Why It Happens:** Quickbase allows at most 50 webhook messages per second per app, shared with Pipelines listening on the same app, and won't send a message larger than 4 MB. Messages past either limit are not sent; the app manager gets an alert and an email.

**Workarounds:**

* Narrow triggers with **Additional criteria** so only meaningful changes fire.
* Send IDs and the fields you need, not whole records, to stay well under 4 MB.
* Reconcile against the Quickbase API after large imports.

**How Hookdeck Can Help:** Hookdeck accepts bursts as fast as Quickbase sends them and delivers to your service at a [max delivery rate](/docs/destinations#set-a-max-delivery-rate) you choose, so your endpoint never becomes the bottleneck.

### No documented retries

**The Problem:** A request your endpoint fails during a deploy may never be resent.

**Why It Happens:** Quickbase logs failures in the error history and emails the webhook owner, but its documentation doesn't describe a retry policy or timeout.

**Workarounds:**

* Keep the endpoint simple and available, and make sure the webhook owner reads the failure emails.
* Review the 30-day **View error history** list after incidents.

**How Hookdeck Can Help:** Hookdeck stores each request it accepts and delivers it to your service with [retries](/docs/retries) on your schedule, and you can replay any request after a fix.

### Webhooks that stop firing

**The Problem:** A webhook that worked yesterday sends nothing today.

**Why It Happens:** The owner must have permission to the table, be a registered user and not be on the deny list, or the webhook isn't sent. Copied apps carry their webhooks over as inactive, and sharing an app to the Exchange removes them.

**Workarounds:**

* Make a long-lived service account the owner of production webhooks.
* After copying an app, select the copied webhooks and **Activate** them.

**How Hookdeck Can Help:** [Issues](/docs/issues) and Hookdeck's request history make a sudden drop in traffic visible, so a silent webhook gets noticed.

### Loops

**The Problem:** A webhook that writes back to its own table triggers itself again and again.

**Why It Happens:** An update made by the receiver is a record change like any other.

**Workarounds:**

* Use **Additional criteria** so the receiver's own update can't match the trigger, as Quickbase advises.

**How Hookdeck Can Help:** [Filters](/docs/filters) can drop requests that match your receiver's own changes before they reach it.

## Best practices

### Put `%action%` and `%webhookid%` in every body

They tell the receiver what happened and which webhook sent it, which Quickbase otherwise doesn't.

### Keep the secret in a header

Add it under **Message header** and compare it whole, in constant time. Never put it in the URL or the body.

### Prefer `POST`

It is the default and what most receivers expect. If you choose another method, allow it on the Hookdeck source and in your route.

### Deduplicate

Include the record ID and `%curdate%` in the body and build an idempotency key from them. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

### Acknowledge fast and process asynchronously

Store the message and respond, then do the work. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

## Conclusion

A Quickbase table webhook sends a request you design when records are added, modified or deleted, with no signature and no Quickbase-defined fields. Authenticate it with a secret header, put `%action%` and identifiers in the body, stay inside the 50-per-second and 4 MB limits, and allow any non-`POST` method on your receiver and on the Hookdeck source.

[Hookdeck Event Gateway](https://hookdeck.com) checks the header, absorbs bursts, and keeps every request until your service has processed it.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Quickbase webhooks reliably in minutes.