Agent skill

Loop Webhooks Skill

Receive and verify Loop webhooks (loop.com, freight audit and logistics payments). Use when setting up a Loop webhook handler, debugging Loop svix-id / svix-timestamp / svix-signature verification, deduplicating on eventId, or handling events like payment.completed, payment.failed, receivable-invoice.finalized, payable-invoice-review.completed, shipment-job.rerated, business-exception.created or test-event.created. Not Loop Returns or Loop Subscriptions (Shopify apps from different companies).

Install this skill

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


Loop (loop.com) is a freight audit and logistics payments platform. It sends webhooks for payments, payable and receivable invoices, shipment jobs, document artifacts and business exceptions. Delivery runs on Svix, so every webhook carries svix-id, svix-timestamp and svix-signature headers.

Not Loop Returns or Loop Subscriptions. Those are Shopify apps from different companies with their own webhook schemes. This skill covers docs.loop.com only.

When to Use This Skill

  • How do I receive Loop webhooks?
  • How do I verify a Loop webhook signature?
  • Why is my Loop svix-signature verification failing?
  • How do I handle payment.completed or receivable-invoice.finalized events?
  • How do I deduplicate Loop webhook deliveries?
  • How do I send a Loop test event (test-event.created)?

Signature Verification

Headerssvix-id, svix-timestamp (Unix seconds), svix-signature
AlgorithmHMAC-SHA256, base64-encoded
Signed content{svix-id}.{svix-timestamp}.{raw body}
KeySigning secret minus the whsec_ prefix, base64-decoded
Header formatSpace-separated v1,<sig> entries; accept if any matches
ToleranceSvix SDKs reject timestamps more than 5 minutes off

Loop's docs recommend the official Svix libraries (npm i svix, pip install svix).

Verification (core)

import { Webhook } from 'svix'; // svix 2.x is ESM-only

// rawBody: the exact string/Buffer Loop sent, NOT JSON.stringify(req.body)
new Webhook(process.env.LOOP_WEBHOOK_SECRET).verify(rawBody, {
  'svix-id': headers['svix-id'],
  'svix-timestamp': headers['svix-timestamp'],
  'svix-signature': headers['svix-signature'],
}); // throws on a bad signature or stale timestamp -> return 400

What the SDK computes, if you need to do it by hand:

const key = Buffer.from(secret.slice('whsec_'.length), 'base64');
const expected = crypto.createHmac('sha256', key)
  .update(`${svixId}.${svixTimestamp}.${rawBody}`).digest('base64');
const ok = svixSignature.split(' ').some((entry) => {
  const [version, sig] = entry.split(',');
  const a = Buffer.from(sig ?? ''), b = Buffer.from(expected);
  return version === 'v1' && a.length === b.length && crypto.timingSafeEqual(a, b);
}); // also reject if |now - svixTimestamp| > 300 seconds

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

Payload and Deduplication

Payloads are flat: no type/data envelope. eventType, eventId and eventCreatedAt sit at the top level beside the entity QIDs:

{
  "eventCreatedAt": "2023-01-01T00:00:00.000Z",
  "eventId": "JG6_GkHkURAUFawN4JTV_4UPaTNvWbIU1KAcgsvuv1s",
  "eventType": "payment.completed",
  "initiatedAt": "2023-01-01T00:00:00.000Z",
  "completedAt": "2023-01-01T00:00:00.000Z",
  "paymentDisplayId": "LP12345678",
  "paymentQid": "qid::payment:0ca6f700-4137-4d09-b44a-43bb8d7900c4"
}
  • Deduplicate on the payload eventId, not svix-id. Loop says eventId is stable across republishes, while svix-id identifies one delivery message and can change if Loop resends an event. Delivery is at least once.
  • Order is not guaranteed. Track the latest eventCreatedAt per entity, or simply refetch the entity from the Loop API. Loop's guidance is to treat the payload as a notification, not a source of truth.
  • Respond 2xx within 15 seconds; queue slow work.

Event Types

GroupEvents
Paymentspayment.initiated, payment.completed, payment.failed
Receivable invoicesreceivable-invoice.created, receivable-invoice.finalized, receivable-invoice.voided
Payable invoicespayable-invoice-review.completed, payable-invoice.shipment-job-linked, payable-invoice.shipment-job-unlinked
Shipment jobsshipment-job.normalized, shipment-job.rerated
Shipment job artifactsshipment-job-artifact.assigned, shipment-job-artifact.unassigned, shipment-job-artifact.assessed, shipment-job-artifact.unassessed
Artifactsartifact.root-ingestion-completed
Business exceptionsbusiness-exception.created, business-exception.resolved, business-exception.reopened, business-exception.deleted
QuickPayquick-pay-relationship.revised
Testingtest-event.created

Note the hyphens inside resource names (receivable-invoice, shipment-job-artifact). See references/overview.md for each event's fields.

Environment Variables

# Endpoint signing secret from Loop's webhook portal ("whsec_" + base64)
LOOP_WEBHOOK_SECRET=whsec_your_signing_secret_here

Local Development

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

No account required. The CLI creates a guest account on first run and gives you a public URL plus a web UI for inspecting requests. Add that URL as an endpoint in Loop, then send test-event.created from the endpoint's Testing tab.

Reference Materials


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Oct 28, 2026

View on GitHub →