# Guide to Firstbase Webhooks: Features and Best Practices

Firstbase webhooks tell your systems when equipment moves. A laptop order is created, needs approval, ships or is delivered; a returned kit arrives back at the processing facility; an inventory order item is received. If you equip remote employees through Firstbase, webhooks let your HRIS, IT asset register and onboarding workflows react to those changes instead of polling the Firstbase API.

This guide covers how Firstbase webhooks are configured, why their payloads carry only IDs, how to authenticate them with a custom header, and the timeout and retry rules your receiver has to meet.

## What are Firstbase webhooks?

Each Firstbase webhook covers one resource type: orders, return orders or inventory orders. You choose which events in that category trigger it, and optionally filter by region or destination. When a matching event happens, Firstbase sends a JSON `POST` to your HTTPS URL.

The payload is deliberately **thin**. It says which resource changed and whether it was created, updated or deleted, but not which granular event fired. Your receiver fetches the resource from the Firstbase API to find out its current status.

Firstbase doesn't sign webhooks. A webhook can authenticate to your endpoint with no headers, an auth token, a custom header, or OAuth client credentials. For a receiver that checks a static secret, choose **Custom header**: you set both the header name and its value, so you know exactly what to compare.

## Firstbase webhook features

| Feature | Details |
| --- | --- |
| Configuration | **API & Integrations** > **Webhooks** > **Add Webhook** (admin permissions) |
| Resource types | Order events, Return order events, Inventory order events |
| Filters | Region or country. Destination type for orders, return destination for returns |
| Method and format | `POST`, JSON |
| Payload | `eventId`, `eventType`, `resourceType`, `resourceId` |
| Authentication | None, Auth token, Custom header or OAuth client credentials. No signature |
| Timeout | 10 seconds |
| Success | `200`, `201`, `202` or `204` |
| Retries | Up to 12 attempts over about 2 to 3 hours, backoff up to 1 hour apart |
| After retries | Marked failed. **Resend** manually from the Activity tab |
| Delivery history | 90 days, with request, response and status code |
| Limits | No limit on webhooks. One webhook per URL and resource type |

## Common events

| Resource type | Events you can select |
| --- | --- |
| Orders | Created, requires approval, shipped, delivered, delivery exception, cancelled |
| Return orders | Submitted, confirmed, cancelled, completed, marked as not returned, kit sent, kit sent back, arrived at processing facility |
| Inventory orders | Created, item shipped, item received, item delivery exception, item cancelled, shipment item cancelled, cancelled, arrived at processing facility |

These selections decide whether Firstbase sends a notification. They aren't in the payload. Every notification looks like this:

```json
{
  "eventId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "eventType": "UPDATED",
  "resourceType": "ORDER",
  "resourceId": "550e8400-e29b-41d4-a716-446655440000"
}

```

* `eventId` is unique per event. Firstbase says the same event may be delivered more than once during retries, so use it to deduplicate.
* `eventType` is `CREATED`, `UPDATED` or `DELETED`.
* `resourceType` is `ORDER`, `RETURN_ORDER` or `INVENTORY_ORDER`.
* `resourceId` is the order, return or inventory order to fetch.

To tell a shipped order from a delivered one, fetch it and read its status: `GET /api/v1/orders/{orderId}`, `GET /api/v1/returns/{returnId}` or `GET /api/v1/inventory-orders/{inventoryOrderId}`.

The **Test** button sends a different body, `{"type": "webhook.test", "timestamp": "..."}`, so make sure your receiver acknowledges it rather than rejecting a body with no `eventId`.

> **Inspect Firstbase webhooks as they arrive.** Point a webhook at a [Hookdeck Inspect](https://inspect.hookdeck.com) URL and click **Test** to see the body and headers Firstbase sends, with no account or setup required.

## Setting up Firstbase webhooks

Pick a header name, such as `Authorization` or `X-Firstbase-Secret`, and generate a long random value, for example with `openssl rand -hex 32`.

1. Go to **API & Integrations** and click **Webhooks**.
2. Click **Add Webhook**.
3. Enter a **Name** and choose the **Resource Type**.
4. Leave **All events** on, or turn it off and select specific events.
5. Enter the HTTPS **Target URL**.
6. Under **Authentication**, choose **Custom header** and enter the header name and value.
7. Apply any region or destination filters, then click **Add Webhook**.
8. Make sure the webhook's toggle is on, and click **Test**.

To cover orders, returns and inventory orders on one URL, create three webhooks, one per resource type. Firstbase won't create two webhooks with the same URL and resource type.

To route through Hookdeck, create a source with the **Firstbase** type and use its URL in step 5. Enter the header name in **API Key Header** and the exact value, including any `Bearer ` prefix, in **API Key**. As Hookdeck's setup note says, use Custom header authentication, not Auth token or OAuth: Firstbase doesn't document which header Auth token sends, and OAuth client credentials need a token server that issues access tokens. The source accepts `POST`.

## Securing Firstbase webhooks

Look up the header you configured and compare its whole value with the stored secret, in constant time.

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

const HEADER_NAME = (process.env.FIRSTBASE_WEBHOOK_HEADER || 'authorization').toLowerCase();
const EXPECTED = process.env.FIRSTBASE_WEBHOOK_SECRET;

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();

app.post('/webhooks/firstbase', express.raw({ type: '*/*' }), (req, res) => {
  const received = req.get(HEADER_NAME);
  if (!EXPECTED || typeof received !== 'string' || !safeEqual(received, EXPECTED)) {
    return res.sendStatus(401);
  }

  let event;
  try {
    event = JSON.parse(req.body.toString('utf8'));
  } catch {
    return res.sendStatus(400);
  }
  if (event.type === 'webhook.test') return res.sendStatus(200);

  // Store event.eventId, queue a job to fetch event.resourceType / event.resourceId,
  // and respond within 10 seconds.
  res.sendStatus(200);
});

```

The same check in Python:

```python
import hmac
import os

HEADER_NAME = os.environ.get("FIRSTBASE_WEBHOOK_HEADER", "Authorization")
EXPECTED = os.environ.get("FIRSTBASE_WEBHOOK_SECRET", "")

def verify_firstbase_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 static header authenticates the sender, not the body. It cannot show that a payload wasn't modified, and with no timestamp a captured request can be replayed and still pass. The thin payload limits the damage: a forged notification can only make you fetch a resource, and the data you act on comes from the authenticated Firstbase API. Still, use HTTPS, a long random value, and change it in the webhook settings if it might have leaked.

> **Make Firstbase webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) checks your custom header, answers Firstbase inside its 10-second timeout, and queues every notification durably while your service fetches the details.

## Firstbase webhook limitations and pain points

### The payload doesn't say what happened

**The Problem:** You subscribed to "order shipped" and "order delivered", and both arrive as `UPDATED` on an `ORDER`.

**Why It Happens:** Firstbase sends only IDs and a coarse `eventType`, so the receiver gets the current data from the API rather than from the webhook.

**Workarounds:**

* Fetch the resource for every notification and act on its current status.
* Select only the granular events you act on, so you fetch fewer resources you would discard.

**How Hookdeck Can Help:** Hookdeck queues notifications and delivers them at a [max delivery rate](/docs/destinations#set-a-max-delivery-rate), so fetching every resource doesn't overload your service or the Firstbase API. See [handling thin events with Hookdeck](/docs/guides/handling-thin-events-with-hookdeck).

### Ten seconds, then a retry

**The Problem:** A handler that calls the Firstbase API before responding times out and receives the same event again.

**Why It Happens:** Firstbase waits 10 seconds for a `2xx`. A timeout, `4xx` or `5xx` triggers retries: up to 12 attempts over 2 to 3 hours, at 5, 10, 20 and 40 seconds and growing to an hour apart.

**Workarounds:**

* Store the notification and respond first, then fetch the resource in a background job.
* Deduplicate on `eventId`.

**How Hookdeck Can Help:** Hookdeck answers Firstbase as soon as a notification arrives and delivers it to your service with a [delivery timeout](/docs/destinations#set-a-delivery-timeout) you choose, so slow fetches no longer cause Firstbase retries.

### Retries end after a few hours

**The Problem:** An outage longer than an afternoon leaves notifications marked as failed.

**Why It Happens:** After 12 attempts, Firstbase stops and waits for someone to click **Resend** in the Activity tab, which keeps 90 days of history.

**Workarounds:**

* Watch the Activity tab's **Failed** filter after incidents and resend.
* Reconcile open orders and returns against the API periodically.

**How Hookdeck Can Help:** Hookdeck's ingestion stays up when your service is down, so Firstbase's deliveries succeed. Hookdeck then [retries](/docs/retries) delivery to your service on your schedule, and you can replay any notification.

### Authentication options that don't fit a static check

**The Problem:** You pick **Auth token** or **OAuth client credentials** and can't tell what your receiver should compare.

**Why It Happens:** Firstbase says Auth token "sends a bearer/token value you provide" without naming the header, and with OAuth client credentials Firstbase requests an access token from your token URL and uses it when delivering events.

**Workarounds:**

* Use **Custom header**, which sends a header name and value you choose.

**How Hookdeck Can Help:** Hookdeck's Firstbase source checks the custom header you configure. Hookdeck answers Firstbase with a `200` either way, but a request with a missing or wrong header is recorded as failed verification and never reaches your service.

## Best practices

### Fetch, don't trust

Treat each notification as a pointer. Fetch the resource from the Firstbase API and act on what it returns.

### Deduplicate on `eventId`

Firstbase says retries can deliver the same event twice. Store `eventId` before processing. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

### Handle the test body

Acknowledge `{"type": "webhook.test"}` with a `2xx` and do nothing else.

### Use filters at the source

Region and destination filters cut notifications you would otherwise fetch and discard.

### Acknowledge fast and process asynchronously

Ten seconds isn't enough for an API call chain. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

## Conclusion

Firstbase sends thin JSON notifications for orders, returns and inventory orders, authenticated by whatever you choose under **Authentication**. Choose **Custom header**, compare it whole in constant time, deduplicate on `eventId`, fetch each resource for its real status, and respond within 10 seconds.

[Hookdeck Event Gateway](https://hookdeck.com) checks the header, answers Firstbase immediately, and paces delivery so your service can fetch details at its own speed.

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