# 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)

```javascript
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/](https://github.com/hookdeck/webhook-skills/tree/main/skills/particle-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/particle-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/particle-webhooks/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

```bash
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):

```bash
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](https://github.com/hookdeck/webhook-skills/blob/main/skills/particle-webhooks/references/overview.md) - What Particle webhooks are, body formats and events
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/particle-webhooks/references/setup.md) - Create the integration, add the secret header, methods
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/particle-webhooks/references/verification.md) - Header check details, gotchas, debugging 401s
* [examples/](https://github.com/hookdeck/webhook-skills/tree/main/skills/particle-webhooks/examples/) - Runnable Express, Next.js and FastAPI handlers with tests