Agent skill

Currents Webhooks Skill

Receive and authenticate Currents HTTP webhooks. Use when setting up a Currents (currents.dev) webhook endpoint for Playwright test runs, checking the secret custom header, debugging 401s, or handling RUN_START, RUN_FINISH, RUN_TIMEOUT and RUN_CANCELED events.

Install this skill

npx skills add hookdeck/webhook-skills --skill currents-webhooks


Currents (currents.dev) reports Playwright test runs. Its HTTP Webhook integration sends a JSON POST when a run starts, finishes, times out or is canceled. Webhooks are not signed. You authenticate them with a secret header that you add to the integration's Headers field; your endpoint compares that header with the value you stored.

The header name is your choice, so the examples read it from CURRENTS_WEBHOOK_HEADER (default Authorization) and the expected value from CURRENTS_WEBHOOK_SECRET.

When to Use This Skill

  • How do I receive Currents webhooks?
  • How do I authenticate a Currents HTTP webhook? (No signature: use a secret custom header)
  • How do I handle RUN_START, RUN_FINISH, RUN_TIMEOUT or RUN_CANCELED events?
  • Why does my Currents webhook endpoint return 401?
  • What is the x-timestamp header on Currents webhooks?

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 verifyCurrentsRequest(
  headers,
  secret = process.env.CURRENTS_WEBHOOK_SECRET,
  headerName = process.env.CURRENTS_WEBHOOK_HEADER || 'Authorization'
) {
  if (!secret) return false;                          // never accept when unconfigured
  const received = headers[headerName.toLowerCase()]; // Node lowercases header names
  return Boolean(received) && safeEqual(received, secret); // WHOLE value, no prefix parsing
}
  • Compare the whole header value. If you typed Bearer abc123 into Currents, then CURRENTS_WEBHOOK_SECRET is Bearer abc123. Don't strip or parse the prefix.
  • Look the header up case-insensitively. Return 401 on a missing header or a mismatch.
  • Read the body raw (express.raw({ type: '*/*' })) so any content type works, and parse the JSON only after the check passes.
  • A static shared credential authenticates the sender, not the body, and gives no replay protection (no signature, and the only timestamp is unsigned). Use HTTPS and rotate the credential if it leaks.
  • Currents also sends an x-timestamp header with the system's epoch timestamp, which its docs offer to help you prevent replay attacks. It is not signed, so anyone holding the secret header can set a fresh value: treat it as a freshness hint at most, and handle events idempotently.

For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.

Event Types

The payload's event field is one of these (exact strings, upper case):

EventSent When
RUN_STARTA new run starts
RUN_FINISHA run finishes
RUN_TIMEOUTA run times out
RUN_CANCELEDA run is canceled

Runs with several groups (multiple Playwright projects under one ci-build-id) send one request per group, each with its own groupId.

Environment Variables

CURRENTS_WEBHOOK_HEADER=Authorization       # header name you added in the integration's Headers
CURRENTS_WEBHOOK_SECRET=your-secret-value   # exact, whole header value

Local Development

Currents can't reach localhost. Tunnel with the Hookdeck CLI (no account required):

npx hookdeck-cli listen 3000 currents --path /webhooks/currents

Use the printed URL as the webhook URL in Currents.

Resources


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 3, 2026

View on GitHub →