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_ 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:
{
"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.
- 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.
- Create a custom webhook with the event name prefix and your HTTPS URL.
- Choose the request type and format.
POSTwith JSON suits most receivers. - Under headers, add your header with the static secret value.
- Save, then publish a matching event from a device and watch for
hook-sentandhook-responsein the console's event log.
The same webhook as JSON, for the CLI or the custom template editor:
{
"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.
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:
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 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
200quickly and process asynchronously. - Watch for
hook-errorevents in the console or withparticle 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 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_atrather than arrival time.
How Hookdeck Can Help: Hookdeck 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 and our guide to 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 checks the header, answers Particle immediately, and holds every device event until your service has processed it.
Get started with Hookdeck for free and handle Particle webhooks reliably in minutes.