# Guide to Particle Webhooks: Features and Best Practices

Particle webhooks connect devices to the rest of your stack. When a device publishes an event, such as a temperature reading, a motion alert or a status update, the Particle Device Cloud turns it into an HTTP request to your server. If you build connected products on Particle, webhooks are how device data reaches your databases, dashboards and alerting without devices talking to your infrastructure directly.

This guide covers how Particle webhooks are triggered and shaped, how to authenticate requests that Particle doesn't sign, how throttling and hook responses work, and the practices that keep a receiver reliable.

## What are Particle webhooks?

A Particle webhook is an integration with an event name, a URL and request settings. When any device it covers publishes an event whose name starts with the webhook's event name, Particle sends a request. The match is a case-sensitive prefix, so a webhook on `test` also fires for `testing` and `test1`. There is no wildcard, and the event name needs at least one character.

Particle doesn't define event types. Your firmware chooses event names and data with `Particle.publish()`, and you decide the request's shape in the webhook. With only an event name and URL, the request is a form-encoded `POST`:

```
POST / HTTP/1.1
User-Agent: ParticleBot/1.1 (https://docs.particle.io/webhooks)
host: host.com
content-type: application/x-www-form-urlencoded

event=demo&data=mydata&published_at=2017-02-01T21%3A08%3A05.763Z&coreid=3f002b000000000000000000

```

The default fields are `event` (the event name), `data`, `published_at` and `coreid` (the device ID). Particle adds them to any form, JSON or query data you configure, unless you set `noDefaults`.

Requests are not signed. Particle's docs say webhooks are "typically" authenticated "using HTTP basic authentication or by using an Authorization header and a bearer token", and both are set on the webhook.

Particle also sends your server's response back to the device. The response is published as `hook-response/<event name>/<index>` events in 512-byte chunks, and errors as `hook-error` events, so firmware can subscribe to them.

## Particle webhook features

| Feature | Details |
| --- | --- |
| Configuration | Console **Cloud Services > Integrations**, `particle webhook create`, or `POST /v1/webhooks` |
| Scope | Sandbox, product, organization or organization product |
| Trigger | Case-sensitive event name prefix |
| Request type | `GET`, `POST`, `PUT` or `DELETE`. Default `POST` |
| Request format | Web form (default), JSON or custom body for `POST` and `PUT`. Query parameters for any type |
| Templates | Mustache variables such as `{{{PARTICLE_EVENT_VALUE}}}` and `{{PARTICLE_DEVICE_ID}}` |
| Authentication | HTTP Basic auth, or custom headers |
| Signature | None |
| Timeout | 20 seconds |
| Throttling | Requests to a failing hostname are skipped, retried after 30 seconds and 1 minute, then dropped |
| Delivery | At least once. Order not guaranteed |
| Responses | Published to devices as `hook-response` and `hook-error` events |

## What a request contains

Because you build the request, it can look like anything. Particle's docs recommend JSON when you're sending to your own service. This webhook, with `noDefaults`, sends only what the template builds:

```json
{
  "event": "Temperature/",
  "url": "https://example.com/webhooks/particle",
  "requestType": "POST",
  "json": {
    "{{PARTICLE_EVENT_NAME}}": "{{{PARTICLE_EVENT_VALUE}}}"
  },
  "noDefaults": true
}

```

A device calling `Particle.publish("Temperature/Kitchen", "20.00", PRIVATE)` then produces the body `{"Temperature/Kitchen":"20.00"}`.

For a general-purpose receiver, keep the defaults or include the event name, device ID and publish time in your JSON so every request is self-describing. Use triple braces, `{{{var}}}`, for values that shouldn't be HTML-escaped.

## Setting up Particle webhooks

Pick a header name and a long random value first, for example `X-Particle-Webhook-Secret` with the output of `openssl rand -hex 32`, and store the value server-side.

1. In the Particle console, select your product or sandbox, then **Cloud Services > Integrations** in the sidebar. Create integrations inside a product when you have one, so they also work for unclaimed product devices.
2. Create a custom webhook with the event name prefix and your HTTPS URL.
3. Choose the request type and format. `POST` with JSON suits most receivers.
4. Under headers, add your header with the static secret value.
5. Save, then publish a matching event from a device and watch for `hook-sent` and `hook-response` in the console's event log.

The same webhook as JSON, for the CLI or the custom template editor:

```json
{
  "event": "sensor/",
  "url": "https://example.com/webhooks/particle",
  "requestType": "POST",
  "json": {
    "event": "{{{PARTICLE_EVENT_NAME}}}",
    "event_id": "{{{PARTICLE_EVENT_ID}}}",
    "data": "{{{PARTICLE_EVENT_VALUE}}}",
    "device_id": "{{{PARTICLE_DEVICE_ID}}}",
    "published_at": "{{{PARTICLE_PUBLISHED_AT}}}"
  },
  "headers": {
    "X-Particle-Webhook-Secret": "your-long-random-value"
  }
}

```

**With Hookdeck.** Particle is a supported source in Hookdeck. Create a source with the Particle type, enter your header name as the **API Key Header** and the value as the **API Key**, and use the Hookdeck source URL as the webhook URL. Hookdeck's setup note says to add a custom header to the webhook integration and to use a static value, not a mustache variable, since a templated value changes with each request. Basic auth isn't covered by this source type, so use a custom header. The source pre-selects `POST` without managing the method. If your webhook uses `GET`, `PUT` or `DELETE`, allow that method on the source as well, because the source only accepts the methods you allow. A request without the right header still gets a `200` from Hookdeck, but Hookdeck marks it as failed verification and doesn't deliver it to your service.

## Securing Particle webhooks

Compare the custom header with the stored value in constant time. Look the header up case-insensitively, compare the whole value, and check lengths before calling `crypto.timingSafeEqual`.

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

function safeEqual(a, b) {
  const x = Buffer.from(String(a), 'utf8');
  const y = Buffer.from(String(b), 'utf8');
  return x.length === y.length && crypto.timingSafeEqual(x, y);
}

function verifyParticleRequest(headers) {
  const secret = process.env.PARTICLE_WEBHOOK_SECRET;
  if (!secret) return false; // fail closed when unconfigured
  const name = (process.env.PARTICLE_WEBHOOK_HEADER || 'X-Particle-Webhook-Secret').toLowerCase();
  const received = headers[name];
  return typeof received === 'string' && safeEqual(received, secret);
}

const app = express();

app.post('/webhooks/particle', express.raw({ type: '*/*' }), (req, res) => {
  if (!verifyParticleRequest(req.headers)) {
    return res.sendStatus(401);
  }

  const raw = req.body.toString('utf8');
  const type = req.get('content-type') || '';
  const event = type.includes('application/json')
    ? JSON.parse(raw)
    : Object.fromEntries(new URLSearchParams(raw)); // the default web form
  // event.event, event.data, event.coreid (or your own JSON fields)
  res.sendStatus(200);
});

```

The same check in Python:

```python
import hmac
import os

def verify_particle_request(headers) -> bool:
    secret = os.environ.get("PARTICLE_WEBHOOK_SECRET")
    if not secret:
        return False
    name = os.environ.get("PARTICLE_WEBHOOK_HEADER", "X-Particle-Webhook-Secret")
    received = headers.get(name)  # case-insensitive in Starlette and Flask
    if not received:
        return False
    return hmac.compare_digest(received.encode("utf-8"), secret.encode("utf-8"))

```

A static header proves the sender knows the value, but it doesn't prove the body is unchanged, and it gives no replay protection: nothing is signed, and `published_at` is only as trustworthy as the request. Particle tells you to use an HTTPS URL whenever you send credentials, so do. Keep the value long and random, keep it out of firmware, which never needs it, and replace it on the webhook if it might have leaked.

> **Make Particle webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) checks your header on every Particle request, stores each device event, and answers immediately, so a slow or failing service never gets your hostname throttled.

## Particle webhook limitations and pain points

### Failing servers get throttled, then events are dropped

**The Problem:** During an outage, device events stop arriving and some are never delivered.

**Why It Happens:** Particle skips webhook requests to a hostname that returns more `4xx` or `5xx` responses than `2xx` ones. A request that takes longer than 20 seconds also counts as a failure. Skipped events are retried after 30 seconds and 1 minute, then dropped, and the device's event stream shows a `hook-error` reading "Sleeping, too many errors, please wait and try again".

**Workarounds:**

* Return `200` quickly and process asynchronously.
* Watch for `hook-error` events in the console or with `particle subscribe`.
* For data you can't lose, have your server acknowledge receipt to the device, as Particle suggests, and let firmware republish.

**How Hookdeck Can Help:** Hookdeck accepts and stores each request as soon as it arrives, so your hostname never looks unhealthy to Particle. It then delivers to your service with [automatic retries](/docs/retries) on a schedule you control.

### Hook responses come from whatever answers the request

**The Problem:** Firmware that waits for data in `hook-response`, such as a weather lookup, receives something unexpected.

**Why It Happens:** Particle publishes the body of the HTTP response back to devices. If an asynchronous gateway answers the request, the device gets the gateway's acknowledgement, not your service's reply.

**Workarounds:**

* Use webhooks through a gateway only for one-way data, such as telemetry and alerts.
* Point request-response webhooks straight at the service that produces the answer.

**How Hookdeck Can Help:** Keep two webhooks: one-way events through Hookdeck for durability, and request-response lookups direct to your service.

### Duplicates and out-of-order events

**The Problem:** The same reading is stored twice, or an older status overwrites a newer one.

**Why It Happens:** Particle says events "are delivered at least once" and are "not guaranteed to be delivered in the order they were sent". A device that loses an acknowledgement can retransmit.

**Workarounds:**

* Include `{{{PARTICLE_EVENT_ID}}}`, which Particle describes as "a unique ID generated for this event", in your JSON and deduplicate on it.
* If a device can republish the same reading, also include your own sequence number in the published data.
* Order by `published_at` rather than arrival time.

**How Hookdeck Can Help:** Hookdeck [deduplication](/docs/deduplication) can drop repeated requests based on the fields you choose.

### Templated credentials can't be checked

**The Problem:** A receiver or gateway can't verify requests whose credential changes per device.

**Why It Happens:** Particle lets header values and Basic auth credentials contain mustache variables, so devices can publish their own credentials. That's useful for some third-party APIs, but there is no fixed value to compare.

**Workarounds:**

* Use a static header value for your own receiver.

**How Hookdeck Can Help:** Hookdeck's Particle source compares the header you name against the exact static value you enter, and logs every request with its headers and body.

## Best practices

### Name events as a hierarchy

Because triggers are prefixes, names like `sensor/temperature` and `sensor/humidity` let one `sensor/` webhook catch both, while a separate `alert/` webhook handles alerts.

### Send JSON with explicit fields

Build a JSON body that includes the event name, event ID, device ID and publish time. A self-describing body is easier to route, deduplicate and replay than a template that only carries the value.

### Mind data operations

Each response chunk a device receives counts as a data operation. If devices don't need the response, don't subscribe to it, and include the device ID in a custom response topic when only the sender should get it.

### Acknowledge fast and process asynchronously

Check the header, store the request, return `200`, then do the work. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks) and our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

## Conclusion

Particle webhooks turn device events matching a name prefix into unsigned HTTP requests whose method and body you configure. Add a static secret header and compare it in constant time over HTTPS, send self-describing JSON, deduplicate because delivery is at least once, and answer quickly so Particle never throttles your hostname.

[Hookdeck Event Gateway](https://hookdeck.com) checks the header, answers Particle immediately, and holds every device event until your service has processed it.

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