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.comonly.
When to Use This Skill
- How do I receive Loop webhooks?
- How do I verify a Loop webhook signature?
- Why is my Loop
svix-signatureverification failing? - How do I handle
payment.completedorreceivable-invoice.finalizedevents? - How do I deduplicate Loop webhook deliveries?
- How do I send a Loop test event (
test-event.created)?
Signature Verification
| Headers | svix-id, svix-timestamp (Unix seconds), svix-signature |
| Algorithm | HMAC-SHA256, base64-encoded |
| Signed content | {svix-id}.{svix-timestamp}.{raw body} |
| Key | Signing secret minus the whsec_ prefix, base64-decoded |
| Header format | Space-separated v1,<sig> entries; accept if any matches |
| Tolerance | Svix 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, notsvix-id. Loop sayseventIdis stable across republishes, whilesvix-ididentifies one delivery message and can change if Loop resends an event. Delivery is at least once. - Order is not guaranteed. Track the latest
eventCreatedAtper 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
2xxwithin 15 seconds; queue slow work.
Event Types
| Group | Events |
|---|---|
| Payments | payment.initiated, payment.completed, payment.failed |
| Receivable invoices | receivable-invoice.created, receivable-invoice.finalized, receivable-invoice.voided |
| Payable invoices | payable-invoice-review.completed, payable-invoice.shipment-job-linked, payable-invoice.shipment-job-unlinked |
| Shipment jobs | shipment-job.normalized, shipment-job.rerated |
| Shipment job artifacts | shipment-job-artifact.assigned, shipment-job-artifact.unassigned, shipment-job-artifact.assessed, shipment-job-artifact.unassessed |
| Artifacts | artifact.root-ingestion-completed |
| Business exceptions | business-exception.created, business-exception.resolved, business-exception.reopened, business-exception.deleted |
| QuickPay | quick-pay-relationship.revised |
| Testing | test-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
- references/overview.md - Every event type with payload fields, dedupe and ordering, retries
- references/setup.md - Adding an endpoint, getting the signing secret, testing, rate limits, recovery
- references/verification.md - Signature algorithm in detail, SDK and manual code, debugging failures