Agent skill
Torii Webhooks Skill
Receive and authenticate requests from Torii workflows. Use when setting up an endpoint for a Torii "Custom action (HTTP Request)", checking a secret header you added to it, debugging 401s, or handling the JSON body a Torii workflow sends (new app discovered, user offboarding and other workflow templates).
Install this skill
npx skills add hookdeck/webhook-skills --skill torii-webhooks
Torii (SaaS management) has no native event webhooks. Outbound requests come from a workflow's Custom action (HTTP Request) step, where the workflow author defines the method, URL, headers and JSON body (with @ placeholders). Requests are not signed and the vendor adds no event field. You authenticate them with a secret header you add to the custom action (Step 7, headers as key-value pairs) and check on your endpoint.
When to Use This Skill
- How do I receive requests from a Torii workflow?
- How do I authenticate a Torii custom action (HTTP Request)?
- Does Torii sign webhook payloads? (No: use a secret header)
- Why does my Torii endpoint return 401?
- How do I handle the JSON body from a Torii workflow, including GET, PUT, PATCH or DELETE?
Verification (core)
const crypto = require('crypto');
// Hash both sides: equal-length buffers, so timingSafeEqual never throws.
function safeEqual(a, b) {
const ha = crypto.createHash('sha256').update(String(a)).digest();
const hb = crypto.createHash('sha256').update(String(b)).digest();
return crypto.timingSafeEqual(ha, hb);
}
function verifyToriiRequest(headers, secret = process.env.TORII_WEBHOOK_SECRET,
name = process.env.TORII_WEBHOOK_HEADER || 'Authorization') {
if (!secret) return false; // never accept when unconfigured
const received = headers[name.toLowerCase()]; // Node lower-cases header names
return typeof received === 'string' && received !== '' && safeEqual(received, secret);
}
- The header name is whatever you typed in Torii; set it in
TORII_WEBHOOK_HEADER(defaultAuthorization). Look it up case-insensitively. - Compare the whole value with
TORII_WEBHOOK_SECRET, including anyBearerprefix you typed. Do not strip or parse a prefix. - Return
401for a missing header or mismatch,400for a non-empty body that isn't JSON,200once handled. An empty body is valid. - Read the body raw (
express.raw({ type: '*/*' })) and authenticate before parsing it. - Accept every method the workflow might use: POST, PUT, PATCH, GET and DELETE. Hookdeck's Torii source pre-selects POST without managing the method, so allow any other method the sender uses on the source too.
- 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.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Payload
There is no fixed schema or event type. The body is the JSON template the workflow author writes, with @ placeholders filled in by Torii. The examples dispatch on an illustrative workflow field (new app discovered, user offboarding) that you would define yourself in the template. See references/overview.md.
Environment Variables
TORII_WEBHOOK_HEADER=Authorization # header name you added in Torii
TORII_WEBHOOK_SECRET="Bearer your-long-random-secret" # exact whole header value
Local Development
Torii can't reach localhost. Tunnel with the Hookdeck CLI (no account required):
npx hookdeck-cli listen 3000 torii --path /webhooks/torii
Use the printed URL as the custom action URL in Torii.
Resources
- references/overview.md - How Torii custom actions send requests, payload shape
- references/setup.md - Add the custom action, header and body in a workflow
- references/verification.md - Header check details, gotchas, debugging 401s
- examples/ - Runnable Express, Next.js and FastAPI handlers with tests