API & SDK Documentation
Authenticate every request with your API key in the X-API-Key header (a Bearer token works too). The capture URL itself (/api/h/{id}) needs no auth — that is the point: any webhook provider can reach it. Every example below is shown in cURL, JavaScript (fetch) and Python (requests).
1. Create an endpoint
POST /api/v1/endpoints. Account endpoints live 30 days; anonymous demo endpoints live 7. List yours any time with GET /api/v1/endpoints.
curl -X POST "https://gethooklab.dev/api/v1/endpoints" \
-H "X-API-Key: hkl_your_key_here"const res = await fetch("https://gethooklab.dev/api/v1/endpoints", {
method: "POST",
headers: { "X-API-Key": "hkl_your_key_here" },
});
const endpoint = await res.json();
console.log(endpoint.url); // https://gethooklab.dev/api/h/<id>import requests
res = requests.post(
"https://gethooklab.dev/api/v1/endpoints",
headers={"X-API-Key": "hkl_your_key_here"},
)
endpoint = res.json()
print(endpoint["url"]) # https://gethooklab.dev/api/h/<id>{
"id": "k3v9x2m1qa",
"url": "https://gethooklab.dev/api/h/k3v9x2m1qa",
"inspectUrl": "https://gethooklab.dev/inspect/k3v9x2m1qa",
"createdAt": "2026-06-19T18:00:00.000Z",
"expiresInDays": 30
}2. Send it anything
GET, POST, PUT, PATCH and DELETE are all captured: method, query, headers and up to 64KB of body (larger bodies are stored truncated and flagged). Cookie, Authorization and X-API-Key headers are stripped before storage. Each endpoint keeps its last 100 requests. Point any real webhook (Stripe, GitHub, Shopify…) at this URL and watch it arrive in the live inspector (3s polling).
curl -X POST "https://gethooklab.dev/api/h/k3v9x2m1qa?source=stripe" \
-H "Content-Type: application/json" \
-d '{"event":"order.paid","amount":4200}'await fetch("https://gethooklab.dev/api/h/k3v9x2m1qa?source=stripe", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ event: "order.paid", amount: 4200 }),
});requests.post(
"https://gethooklab.dev/api/h/k3v9x2m1qa",
params={"source": "stripe"},
json={"event": "order.paid", "amount": 4200},
)3. List & search captured requests
GET /api/v1/endpoints/{id}/requests. Newest first. Narrow the result with ?q= (case-insensitive grep over method, body, header names/values and query), ?method= (exact verb) and ?limit=(1–100). Filters combine with AND.
curl "https://gethooklab.dev/api/v1/endpoints/k3v9x2m1qa/requests?q=order.paid&method=POST&limit=50" \
-H "X-API-Key: hkl_your_key_here"const params = new URLSearchParams({ q: "order.paid", method: "POST", limit: "50" });
const res = await fetch(
`https://gethooklab.dev/api/v1/endpoints/k3v9x2m1qa/requests?${params}`,
{ headers: { "X-API-Key": "hkl_your_key_here" } },
);
const { requests } = await res.json();res = requests.get(
"https://gethooklab.dev/api/v1/endpoints/k3v9x2m1qa/requests",
headers={"X-API-Key": "hkl_your_key_here"},
params={"q": "order.paid", "method": "POST", "limit": 50},
)
captured = res.json()["requests"]{
"id": "k3v9x2m1qa",
"count": 1,
"query": { "q": "order.paid", "method": "POST" },
"requests": [
{
"id": "req_mbqj0e_a1b2c3",
"method": "POST",
"ts": "2026-06-19T18:00:05.000Z",
"ip": "203.0.113.9",
"query": { "source": "stripe" },
"headers": { "content-type": "application/json" },
"bodyRaw": "{\"event\":\"order.paid\",\"amount\":4200}",
"bodyPretty": "{\n \"event\": \"order.paid\",\n \"amount\": 4200\n}",
"bodyIsJson": true,
"truncated": false,
"contentType": "application/json"
}
]
}4. Export captured requests
GET /api/v1/endpoints/{id}/export?format=json|csv downloads the whole retained window as an attachment. The same ?q= / ?method= filters apply, so you can export exactly the slice you searched. CSV is RFC-4180 escaped and formula-injection safe.
# JSON
curl "https://gethooklab.dev/api/v1/endpoints/k3v9x2m1qa/export?format=json" \
-H "X-API-Key: hkl_your_key_here" -o captures.json
# CSV (only POSTs matching a query)
curl "https://gethooklab.dev/api/v1/endpoints/k3v9x2m1qa/export?format=csv&method=POST&q=order" \
-H "X-API-Key: hkl_your_key_here" -o captures.csvconst res = await fetch(
"https://gethooklab.dev/api/v1/endpoints/k3v9x2m1qa/export?format=csv",
{ headers: { "X-API-Key": "hkl_your_key_here" } },
);
const csv = await res.text();res = requests.get(
"https://gethooklab.dev/api/v1/endpoints/k3v9x2m1qa/export",
headers={"X-API-Key": "hkl_your_key_here"},
params={"format": "csv"},
)
open("captures.csv", "wb").write(res.content)5. Configure a custom response
By default the capture route replies 200 {"ok":true}. Override it per endpoint to simulate a flaky downstream — return a 4xx/5xx, a custom body, or add artificial latency. PATCH /api/v1/endpoints/{id} sets it; DELETE restores the default ack. Status 100–599, body ≤ 8KB, delay 0–15s.
# Make the endpoint answer 503 after a 2s delay
curl -X PATCH "https://gethooklab.dev/api/v1/endpoints/k3v9x2m1qa" \
-H "X-API-Key: hkl_your_key_here" \
-H "Content-Type: application/json" \
-d '{"responseConfig":{"status":503,"body":"{\"error\":\"down\"}","contentType":"application/json","delayMs":2000}}'
# Restore the default ack
curl -X DELETE "https://gethooklab.dev/api/v1/endpoints/k3v9x2m1qa" \
-H "X-API-Key: hkl_your_key_here"await fetch("https://gethooklab.dev/api/v1/endpoints/k3v9x2m1qa", {
method: "PATCH",
headers: {
"X-API-Key": "hkl_your_key_here",
"Content-Type": "application/json",
},
body: JSON.stringify({
responseConfig: { status: 503, body: '{"error":"down"}', contentType: "application/json", delayMs: 2000 },
}),
});requests.patch(
"https://gethooklab.dev/api/v1/endpoints/k3v9x2m1qa",
headers={"X-API-Key": "hkl_your_key_here"},
json={"responseConfig": {
"status": 503,
"body": '{"error":"down"}',
"contentType": "application/json",
"delayMs": 2000,
}},
)6. Replay a request
POST /api/v1/replay. requestIndex is the position in the list above (0 = most recent). The captured method, sanitized headers and body are re-sent to your targetUrl with a 10s timeout. Private/internal targets are rejected (SSRF-guarded, including redirects). Throttled to 10 replays per endpoint per minute.
curl -X POST "https://gethooklab.dev/api/v1/replay" \
-H "X-API-Key: hkl_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"endpointId": "k3v9x2m1qa",
"requestIndex": 0,
"targetUrl": "https://staging.your-app.dev/webhooks/stripe"
}'const res = await fetch("https://gethooklab.dev/api/v1/replay", {
method: "POST",
headers: {
"X-API-Key": "hkl_your_key_here",
"Content-Type": "application/json",
},
body: JSON.stringify({
endpointId: "k3v9x2m1qa",
requestIndex: 0,
targetUrl: "https://staging.your-app.dev/webhooks/stripe",
}),
});requests.post(
"https://gethooklab.dev/api/v1/replay",
headers={"X-API-Key": "hkl_your_key_here"},
json={
"endpointId": "k3v9x2m1qa",
"requestIndex": 0,
"targetUrl": "https://staging.your-app.dev/webhooks/stripe",
},
){
"ok": true,
"replayed": { "endpointId": "k3v9x2m1qa", "requestIndex": 0, "method": "POST" },
"status": 200,
"durationMs": 184,
"bodyPreview": "{\"received\":true}"
}7. Verify a webhook signature
POST /api/v1/verify-signature checks an HMAC-SHA256 signature against a shared secret for Stripe (Stripe-Signature, hex + timestamp tolerance), GitHub (X-Hub-Signature-256, hex) and Shopify (X-Shopify-Hmac-Sha256, base64). Verify a stored capture by id+index (the signature header is pulled automatically), or paste a literal rawBody + signature. Your secret is used in-process only — never stored or logged.
# Verify a captured Shopify webhook by index (header pulled from the capture)
curl -X POST "https://gethooklab.dev/api/v1/verify-signature" \
-H "X-API-Key: hkl_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"provider": "shopify",
"secret": "shpss_your_app_secret",
"endpointId": "k3v9x2m1qa",
"requestIndex": 0
}'
# Verify a literal Stripe payload you pasted
curl -X POST "https://gethooklab.dev/api/v1/verify-signature" \
-H "X-API-Key: hkl_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"provider": "stripe",
"secret": "whsec_...",
"rawBody": "{\"id\":\"evt_1\"}",
"signature": "t=1700000000,v1=abc...",
"toleranceSeconds": 300
}'// GitHub: verify the most recent capture
const res = await fetch("https://gethooklab.dev/api/v1/verify-signature", {
method: "POST",
headers: {
"X-API-Key": "hkl_your_key_here",
"Content-Type": "application/json",
},
body: JSON.stringify({
provider: "github",
secret: process.env.GITHUB_WEBHOOK_SECRET,
endpointId: "k3v9x2m1qa",
requestIndex: 0,
}),
});
const { valid, reason } = await res.json();res = requests.post(
"https://gethooklab.dev/api/v1/verify-signature",
headers={"X-API-Key": "hkl_your_key_here"},
json={
"provider": "shopify",
"secret": "shpss_your_app_secret",
"endpointId": "k3v9x2m1qa",
"requestIndex": 0,
},
)
print(res.json()) # { "valid": true, "reason": "OK", ... }{
"provider": "shopify",
"source": "captured",
"valid": true,
"reason": "OK",
"details": {}
}Failure reasons are machine-readable: SIGNATURE_MISMATCH, TIMESTAMP_OUT_OF_TOLERANCE (Stripe), MALFORMED_SIGNATURE_HEADER, MISSING_SECRET, UNSUPPORTED_SHA1 (GitHub legacy).