← All guides

Secure V2 · SDK 0.3.0

Webhooks and downstream integrations

Updated October 11, 2026 · verified against repository implementation

Configure HTTPS receivers and inspect durable telemetry delivery.

Configure a destination

Open Destinations, provide an HTTPS receiver and choose the applicable device/event types. Owners and admins can configure destinations; viewers inspect permitted metadata. Scale routing uses actual entitlement checks. Invalid or nonmatching routing conditions do not send telemetry.

Telemetry inserts enqueue eligible delivery jobs transactionally. The gateway outbox worker sends them with bounded attempts, backoff and deadlines. Inspect Deliveries and its attempt history; dead jobs can be explicitly replayed within the implemented replay allowance. Connection/disconnection events use separate best-effort behavior.

Verify and deduplicate

Use the destination signing secret to verify x-struct-signature over the exact raw HTTP request body before parsing or acting. Signature is sha256= followed by SHA-256 HMAC in hexadecimal. Compare equal-length digests in constant time. body.id / x-struct-event-id is a stable identity for receiver-side transactional deduplication.

Return HTTP 2xx after your receiver has safely handled the event. Delivery is at least once with bounded attempts: the receiver may commit before Struct observes its response. An authenticated device receipt is not a webhook success. Do not log signing secrets or credential-bearing destination URLs.

// Node.js 22+: verify the exact bytes before parsing. No framework required.

const http = require('node:http');

const {createHmac, timingSafeEqual} = require('node:crypto');

const secret = process.env.STRUCT_WEBHOOK_SECRET;

if (!secret) throw new Error('Set STRUCT_WEBHOOK_SECRET privately');

http.createServer((req, res) => {

  const parts = []; let size = 0;

  req.on('data', b => { size += b.length; if(size > 1048576) req.destroy(); else parts.push(b); });

  req.on('end', () => {

    const raw = Buffer.concat(parts), signature = req.headers['x-struct-signature'];

    if(typeof signature !== 'string' || !/^sha256=[0-9a-f]{64}$/i.test(signature)) {res.writeHead(401).end();return;}

    const actual = Buffer.from(signature.slice(7), 'hex');

    const expected = createHmac('sha256', secret).update(raw).digest();

    if(!timingSafeEqual(actual, expected)) {res.writeHead(401).end();return;}

    try {const event = JSON.parse(raw.toString('utf8'));

      // In production: transactionally deduplicate event.id and store/apply it here.

      // This demonstration prints only the event identity; it is not durable processing.

      console.log('Verified event', event.id);

      res.writeHead(204).end();

    } catch {res.writeHead(400).end();}

  });

}).listen(9000, '127.0.0.1');

// A deployed destination requires your HTTPS endpoint; localhost is for local tests.

Open device setup · Contact support