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).
Prefer a machine-readable contract? The full OpenAPI 3.1 spec is published at https://gethooklab.dev/api/openapi.json — import it into Postman, Insomnia, Swagger UI or any client generator.
1. Create an endpoint
POST /api/v1/endpoints. Your account's first endpoint is permanent — a forever URL that never idle-expires (permanent: true, expiresInDays: null), free tier included. Additional endpoints live 30 days of silence (each capture refreshes the clock); 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",
"permanent": true,
"expiresInDays": null
}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, and every capture refreshes the endpoint's lifetime — an endpoint that keeps receiving traffic never expires. Point any real webhook (Stripe, GitHub, Shopify…) at this URL and watch it arrive in the live inspector.
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},
)Live tail — stream captures in real time (SSE)
GET /api/v1/endpoints/{id}/tail streams every NEW capture as a Server-Sent Event the moment it lands — curl -N turns your terminal into a live webhook console. Each streamed window lasts ~50s and ends with an event: timeout; SSE clients (EventSource, curl --retry) reconnect automatically, so a long session is seamless.
curl -N "https://gethooklab.dev/api/v1/endpoints/k3v9x2m1qa/tail" \
-H "X-API-Key: hkl_your_key_here"
# event: open
# data: {"endpoint":"k3v9x2m1qa","pollMs":1500}
#
# event: capture
# data: {"id":"req_...","method":"POST","bodyPretty":"{\n \"event\": \"order.paid\"...}// Node 18+: stream with fetch (the key travels in a header, never in the URL)
const res = await fetch("https://gethooklab.dev/api/v1/endpoints/k3v9x2m1qa/tail", {
headers: { "X-API-Key": "hkl_your_key_here", Accept: "text/event-stream" },
});
for await (const chunk of res.body.pipeThrough(new TextDecoderStream())) {
process.stdout.write(chunk); // parse SSE frames as needed
}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, as does the same PATCH with {"responseConfig":null}. Status 100–599, body ≤ 8KB, delay 0–15s. Both resets touch only the mock response — to remove the endpoint itself, see section 6.
Failure simulation — test your retry/backoff logic with mode: "fail-first-n" (first failCount captures return failStatus, then the normal response), "flaky" (each capture fails with probability flakyPercent%), or "timeout" (respond after timeoutMs, 100–10000ms). failStatus defaults to 500.
# 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}}'
# Failure simulation: fail the first 2 deliveries with 503, then succeed
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":200,"body":"{\"ok\":true}","mode":"fail-first-n","failCount":2,"failStatus":503}}'
# Restore the default ack
curl -X DELETE "https://gethooklab.dev/api/v1/endpoints/k3v9x2m1qa" \
-H "X-API-Key: hkl_your_key_here"
# ...or the same reset as a PATCH with a null config
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":null}'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. Delete an endpoint
POST /api/v1/endpoints/{id}/delete permanently removes an endpoint and frees a slot against your plan's endpoint limit — this is how you make room when you hit ENDPOINT_LIMIT_REACHED. Owner only. Deletion has its own path rather than reusing DELETE on the endpoint, which resets the mock response (section 5) and always has.
This cannot be undone. The endpoint's captured requests, its custom response, and any forwarding destination or alert webhook attached to it are erased with it. Export anything you still need first (GET /api/v1/endpoints/{id}/export). Deleting an endpoint that is already gone is safe — it answers 200 with {"deleted":false} rather than an error.
One thing survives: any public share link you created from a captured request. Those are independent redacted snapshots, not views onto the endpoint, so they keep working until their own 7-day expiry. Deleting the endpoint does not revoke them.
curl -X POST "https://gethooklab.dev/api/v1/endpoints/k3v9x2m1qa/delete" \
-H "X-API-Key: hkl_your_key_here"
# → {"id":"k3v9x2m1qa","deleted":true}await fetch("https://gethooklab.dev/api/v1/endpoints/k3v9x2m1qa/delete", {
method: "POST",
headers: { "X-API-Key": "hkl_your_key_here" },
});requests.post(
"https://gethooklab.dev/api/v1/endpoints/k3v9x2m1qa/delete",
headers={"X-API-Key": "hkl_your_key_here"},
)7. 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}"
}8. 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).