Agent skill
Tray.io Webhooks Skill
Receive and authenticate requests sent by Tray.io (tray.ai) workflows. Use when setting up an endpoint that a Tray HTTP Client step calls, checking the secret header that authenticates the sender, debugging 401s, or routing a payload whose shape (including any event field) the workflow builder defines.
Install this skill
npx skills add hookdeck/webhook-skills --skill tray-io-webhooks
Tray.io (now tray.ai) does not have a fixed webhook format. Your endpoint is called by the HTTP Client connector in a workflow, so the method, headers and body are whatever the workflow builder configures. Requests are not signed. You authenticate them with a secret header that you add to the HTTP Client step's headers, with the value taken from a Generic Service Authentication ($.auth), and your endpoint compares that header with the value it has stored.
A static shared credential authenticates the sender, not the body. There is no signature and no timestamp, so there is no replay protection and a tampered body can't be detected. Use HTTPS only, and rotate the credential if it leaks.
When to Use This Skill
- How do I receive requests from a Tray.io workflow?
- How do I authenticate a Tray HTTP Client request with a secret header?
- Does Tray.io sign webhook payloads? (No: use a shared secret header)
- How do I route a Tray payload when the workflow, not Tray, defines the body?
- Why does my Tray endpoint return 401 or 400?
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(a).digest();
const hb = crypto.createHash('sha256').update(b).digest();
return crypto.timingSafeEqual(ha, hb);
}
function verifyTrayRequest(headers, {
headerName = process.env.TRAY_IO_WEBHOOK_HEADER || 'Authorization',
secret = process.env.TRAY_IO_WEBHOOK_SECRET,
} = {}) {
if (!secret) return false; // never accept when unconfigured
const received = headers[headerName.toLowerCase()]; // Node lower-cases header names
if (typeof received !== 'string' || !received) return false;
// Compare the WHOLE value (including any "Bearer " prefix); do not strip or parse it.
return safeEqual(Buffer.from(received, 'latin1'), Buffer.from(secret, 'utf8'));
}
- Header name: user-chosen. Read it from
TRAY_IO_WEBHOOK_HEADER(defaultAuthorization) and look it up case-insensitively. - Expected value:
TRAY_IO_WEBHOOK_SECRET, the exact whole header value, including aBearerprefix if the workflow sends one. - Missing header or mismatch:
401. Unconfigured secret: reject everything. - The request method is user-chosen too: accept
POST,PUT,PATCH,DELETEandGET, and accept an empty body (for example aGET). Hookdeck's Tray.io source pre-selectsPOSTwithout managing the method, so allow whichever method your HTTP Client step uses on the source too. - Read the body raw (
express.raw({ type: '*/*' })) and parse JSON only after the credential check. Use the HTTP Client step'srawbody type and setContent-Type: application/jsonas a header, because Tray has no JSON body type and defaults toapplication/x-www-form-urlencodedwhen none is chosen.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Payload (user-defined)
Tray defines no webhook payload and no event types: the body is whatever the HTTP Client step sends. The examples assume you add an event field to the body you build, such as:
{"event": "record.created", "data": {"id": "rec_123", "name": "Acme"}}
The event field and these values are this skill's own convention, not Tray names; replace them with your own.
| Event (illustrative) | Meaning (your choice) |
|---|---|
record.created | A record was created in the source system |
record.updated | A record was changed |
record.deleted | A record was removed |
Environment Variables
TRAY_IO_WEBHOOK_HEADER=Authorization # header name you add in the HTTP Client step
TRAY_IO_WEBHOOK_SECRET=your-secret-value # exact, whole header value (e.g. "Bearer abc123")
Local Development
Tray can't reach localhost. Tunnel with the Hookdeck CLI (no account required):
npx hookdeck-cli listen 3000 tray-io --path /webhooks/tray-io
Use the printed URL as the HTTP Client step's URL.
Resources
- references/overview.md - How Tray HTTP Client requests work, body shape
- references/setup.md - Add the secret header in the HTTP Client step
- references/verification.md - Header check details, limits, debugging 401s
- examples/ - Runnable Express, Next.js and FastAPI handlers with tests