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, default Authorization), looked up case-insensitively. Expected value: PARTICLE_WEBHOOK_SECRET, the exact whole header value, including any Bearer prefix you typed into the integration.
  • Python: hmac.compare_digest(received.encode("latin-1"), secret.encode("utf-8")). Compare bytes, never 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 (auth with username/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:

EventIllustrative meaning
temperatureA device publishes a reading in data
motion-detectedA 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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 11, 2026

View on GitHub →