Agent skill
Particle Webhooks Skill
Receive and authenticate Particle (IoT device cloud) webhooks. Use when setting up a Particle webhook integration endpoint, checking a custom Authorization or secret header, debugging 401s, parsing form-encoded or JSON device events, or handling events published from device firmware such as temperature and motion-detected.
Install this skill
npx skills add hookdeck/webhook-skills --skill particle-webhooks
A Particle webhook integration sends an HTTP request to your URL whenever a device publishes a matching event (Particle.publish("temperature", ...)). Requests are not signed: there is no HMAC and no timestamp. You authenticate them with a secret header that you add to the integration under custom headers (Particle's own example is "headers": {"Authorization": "Bearer 12345"}), and your endpoint compares that header with the value you stored.
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.
When to Use This Skill
- How do I receive Particle webhooks?
- How do I authenticate a Particle webhook integration with a custom header?
- Does Particle sign webhook requests? (No: use a secret header)
- How do I parse Particle's default form-encoded body (
event,data,published_at,coreid)? - Why does my Particle webhook endpoint return 401, or reject GET and PUT requests?
Verification (core)
const crypto = require('crypto');
const sha256 = (buf) => crypto.createHash('sha256').update(buf).digest();
function verifyParticleRequest(
headers,
secret = process.env.PARTICLE_WEBHOOK_SECRET,
headerName = process.env.PARTICLE_WEBHOOK_HEADER || 'Authorization'
) {
if (!secret) return false; // never accept when unconfigured
const received = headers[headerName.toLowerCase()];
if (!received) return false;
// Compare the WHOLE value (no "Bearer " parsing); equal-length digests keep timingSafeEqual safe.
return crypto.timingSafeEqual(
sha256(Buffer.from(received, 'latin1')),
sha256(Buffer.from(secret, 'utf8'))
);
}
- Header name: whatever you chose (
PARTICLE_WEBHOOK_HEADER, defaultAuthorization), looked up case-insensitively. Expected value:PARTICLE_WEBHOOK_SECRET, the exact whole header value, including anyBearerprefix you typed into the integration. - Python:
hmac.compare_. Compare bytes, neverdigest(received.encode("latin-1"), secret.encode("utf-8")) str, which raises on non-ASCII. - Missing header or mismatch:
401. Check it first on every method, before parsing the body. - Read the body raw (
express.raw({ type: '*/*' })) and parse it after the check: form-encoded (the default), JSON, or query-string parameters for GET. An empty body is valid. - Hookdeck's setup note: Add a custom header to the webhook integration and enter it here. Use a static value, not a mustache variable. (Header values may contain mustache variables such as
{{PARTICLE_DEVICE_ID}}, but then the value changes per request and can't be verified.) - Particle integrations can also use HTTP basic auth (
authwithusername/password). Hookdeck's Particle source does not cover basic auth; use a custom header.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Methods
requestType is one of GET, POST, PUT or DELETE (default POST). The examples accept GET, POST, PUT, PATCH and DELETE on the route and authenticate each one. Hookdeck's Particle source pre-selects POST without managing the method, so any other method your integration uses must also be allowed on the source.
Event Types
Event names are chosen by your device firmware, so there is no fixed list. The examples dispatch on the event field with illustrative names:
| Event | Illustrative meaning |
|---|---|
temperature | A device publishes a reading in data |
motion-detected | A device reports motion |
The default form body has four fields: event (the published event name), data (the published value, always a string), published_at (ISO 8601 time) and coreid (the 24-character device ID). JSON, query-string and custom bodies are configurable in the integration.
Environment Variables
PARTICLE_WEBHOOK_HEADER=Authorization # name of the custom header you added
PARTICLE_WEBHOOK_SECRET=Bearer replace-with-a-long-random-string # exact whole header value
Local Development
Particle can't reach localhost. Tunnel with the Hookdeck CLI (no account required):
npx hookdeck-cli listen 3000 particle --path /webhooks/particle
Use the printed URL as the integration URL in the Particle Console.
Resources
- references/overview.md - What Particle webhooks are, body formats and events
- references/setup.md - Create the integration, add the secret header, methods
- references/verification.md - Header check details, gotchas, debugging 401s
- examples/ - Runnable Express, Next.js and FastAPI handlers with tests