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 (default Authorization) and look it up case-insensitively.
  • Expected value: TRAY_IO_WEBHOOK_SECRET, the exact whole header value, including a Bearer prefix 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, DELETE and GET, and accept an empty body (for example a GET). Hookdeck's Tray.io source pre-selects POST without 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's raw body type and set Content-Type: application/json as a header, because Tray has no JSON body type and defaults to application/x-www-form-urlencoded when 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.createdA record was created in the source system
record.updatedA record was changed
record.deletedA 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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 5, 2026

View on GitHub →