Errors & Troubleshooting
Complete reference for webhook HTTP response codes, retry behavior, signature verification errors, and a step-by-step troubleshooting flowchart.
Webhook Errors & Troubleshooting
Webhook delivery is asynchronous. Your endpoint must be publicly accessible, respond within the timeout window, and return the correct HTTP status codes. When something goes wrong — network errors, signature mismatches, or endpoint crashes — Leypal retries delivery automatically using exponential backoff.
This guide covers three categories of issues you may encounter:
- HTTP response codes — what each code means and how Leypal reacts
- Signature verification errors — common causes and fixes for authentication failures
- Delivery issues — reachability, timeouts, and permanent failures
Use the troubleshooting flowchart to diagnose unknown failures, and the production checklist before going live.
What Leypal Sends
Every webhook is an HTTPS POST request with the following properties:
| Property | Value |
|---|---|
| Method | POST |
Content-Type | application/json |
X-Leypal-Signature | HMAC-SHA256 signature of the raw request body |
| Body | JSON payload (see Webhook Events for full structure) |
| Timeout | 30 seconds — Leypal closes the connection after 30 seconds with no response |
Important: Leypal signs the raw request body bytes, not a serialized JSON string. Your endpoint must read the raw body before parsing to verify the signature correctly.
For signature verification implementation details, see Webhook Signature Verification.
HTTP Response Codes
Leypal treats any 2xx response as a successful delivery. Any non-2xx response (including redirects and timeouts) triggers the retry policy.
2xx — Success (No Retry)
| Code | Status | Behavior | Your Action |
|---|---|---|---|
200 | OK | Webhook delivered successfully | Default response — return this when you receive and acknowledge the webhook |
201 | Created | Webhook processed; resource created | Acceptable if your handler creates a resource as part of processing |
202 | Accepted | Webhook queued for background processing | Ideal response — signals you received the payload and will process it asynchronously |
204 | No Content | Webhook received; no response body | Acceptable if you prefer not to send a response body |
Best practice: Return
200or202immediately, before running any business logic. Processing the webhook in the request handler (e.g., querying the database, sending emails) adds latency and risks exceeding the 30-second timeout. Instead, enqueue the event and process it in a background job.
3xx — Redirects (Fail + Retry)
| Code | Status | Behavior | Your Action |
|---|---|---|---|
301 | Moved Permanently | Webhook marked as failed; Leypal does NOT follow redirects | Update webhook URL in dashboard to the final destination |
302 | Found | Same as 301 | Update webhook URL to avoid the redirect |
307 | Temporary Redirect | Same as 301 | Verify endpoint URL is the canonical URL |
Redirects indicate a misconfiguration. Leypal will retry up to 5 times, but each attempt hits the same redirect. Update the webhook URL in your API Key dashboard to point directly to the final endpoint.
4xx — Client Errors (Fail + Retry)
| Code | Status | Behavior | Your Action |
|---|---|---|---|
400 | Bad Request | Endpoint rejected request format | Check your signature verification logic; verify middleware ordering |
401 | Unauthorized | Signature verification failed or missing header | See Signature Verification Errors |
403 | Forbidden | Endpoint exists but rejected request | Check your authorization logic; verify the request is from Leypal |
404 | Not Found | Webhook path doesn't exist | Verify endpoint URL in dashboard; check server routing |
408 | Request Timeout | Endpoint took >30 seconds to respond | Return 202 immediately; move processing to background job |
429 | Too Many Requests | Rate limit exceeded on your endpoint | Implement rate limit exemption for Leypal's IP range, or raise your limits |
All 4xx responses are retried up to 5 times. Leypal assumes the error may be transient (e.g., a brief overload or race condition). Fix the root cause — the retries will eventually succeed or exhaust attempts.
401 and 403 are the most common codes during initial setup. If you see them, go directly to Signature Verification Errors.
5xx — Server Errors (Fail + Retry)
| Code | Status | Behavior | Your Action |
|---|---|---|---|
500 | Internal Server Error | Endpoint threw an unhandled exception | Check server logs; fix the error and redeploy |
502 | Bad Gateway | Reverse proxy couldn't reach your app | Verify app server is running; check nginx/caddy/ALB config |
503 | Service Unavailable | Endpoint overloaded or in maintenance mode | Scale up or reduce processing time per request |
504 | Gateway Timeout | Proxy gave up waiting for your app | Return 202 immediately; reduce handler execution time |
All 5xx responses are retried up to 5 times. These are temporary by definition — the assumption is your server will recover. Check your application logs for stack traces.
Network Errors (Fail + Retry)
| Error Type | Behavior | Your Action |
|---|---|---|
| Connection refused | Retried up to 5 times | Verify endpoint is running and listening on the correct port |
| DNS resolution failure | Retried up to 5 times | Verify domain DNS is configured; check with dig yourendpoint.com |
| Connection timeout | Retried up to 5 times | Verify firewall allows inbound HTTPS (port 443) |
| TLS/SSL error | Retried up to 5 times | Ensure SSL certificate is valid; check certificate chain |
Local development note: Leypal cannot reach
localhostor127.0.0.1. Use ngrok or localtunnel to create a public tunnel to your local server.
Retry Policy
When a webhook delivery fails, Leypal retries automatically with exponential backoff:
| Attempt | Backoff | Cumulative Time After First Failure |
|---|---|---|
| 1 | Initial delivery (no backoff) | 0 min |
| 2 | ~5 minutes | ~5 min |
| 3 | ~10 minutes | ~15 min |
| 4 | ~20 minutes | ~35 min |
| 5 | ~40 minutes | ~75 min |
After the 5th failed attempt, the webhook is marked permanently failed. No further retries are made. To receive the event again, you must trigger the action that caused it (e.g., create a new signature request).
A webhook is marked successful when:
- Leypal receives a
2xxresponse from your endpoint before the 30-second timeout
A webhook is marked failed when:
- All 5 attempts are exhausted without a
2xxresponse - Every attempt results in a network error (unreachable endpoint)
Idempotency note: Because Leypal retries on failure, your endpoint may receive the same event more than once. Always implement idempotent processing — use the
webhookIdfield to detect and skip duplicate deliveries.
Signature Verification Errors
These errors occur when your endpoint returns 401 Unauthorized or when your signature check fails silently.
| Error | Cause | Symptom | Fix |
|---|---|---|---|
| Invalid signature | Computed HMAC doesn't match X-Leypal-Signature header | Endpoint returns 401; logs show signature mismatch | See fixes below |
Missing X-Leypal-Signature header | Webhook not registered or secret not generated | Header absent in req.headers | Verify webhook URL and secret in API Key dashboard |
| Endpoint not reachable | Leypal cannot connect to your URL | Webhook appears as failed in history; responseCode is null | Check firewall, DNS, endpoint availability |
| Invalid algorithm | Using SHA1, MD5, or other algorithm instead of SHA256 | Signature hex string format mismatch | Always use sha256 with Node.js crypto.createHmac |
Fixing "Invalid Signature" (401)
The most common cause is a mismatch between how you compute the HMAC and how Leypal computes it. Check these in order:
1. Wrong or outdated webhook secret
The secret in your .env must exactly match the one in your Leypal API Key dashboard. Even a single extra space or character will cause every webhook to fail.
# Print your current secret (no trailing newline)
echo -n "$WEBHOOK_SECRET" | wc -c
# Compare against the character count shown in your dashboardIf you recently rotated the secret in the dashboard, update your .env and redeploy immediately.
2. Not reading the raw body
The HMAC is computed over the raw request bytes — before JSON parsing. If you let Express parse the body first, the raw bytes are gone and you cannot recompute the correct signature.
// WRONG — JSON parsed body; raw bytes lost
app.use(express.json());
app.post('/webhooks', (req, res) => {
// req.body is already an object; can't get raw bytes back
const sig = computeHmac(webhookSecret, JSON.stringify(req.body)); // FAILS
});
// CORRECT — raw body preserved for webhook route
app.post('/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
// req.body is a Buffer containing the raw bytes
const sig = crypto.createHmac('sha256', webhookSecret)
.update(req.body) // Buffer, not string
.digest('hex');
// now compare with X-Leypal-Signature header
});3. Using string equality instead of timing-safe comparison
// WRONG — timing attack possible
if (computedSig === req.headers['x-leypal-signature']) { ... }
// CORRECT — timing-safe
const a = Buffer.from(computedSig);
const b = Buffer.from(req.headers['x-leypal-signature']);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(401).send('Invalid signature');
}For a complete implementation, see Webhook Signature Verification.
Fixing "Missing X-Leypal-Signature Header"
If the header is absent on every request, the webhook may not be registered:
- Go to your API Key dashboard → Webhooks section
- Verify your endpoint URL is listed and Active
- Verify a webhook secret is generated (click the secret field — it should show
sk_webhook_xxx) - If the secret was deleted or never created, click Generate Secret and copy the new value to your
.env
Webhook Delivery History API
When a webhook is not arriving, the delivery history is the first place to look. It shows exactly what Leypal sent, what your endpoint returned, and why delivery failed.
Endpoint: GET /api/v1/webhook-notifications
Authentication: API key as Bearer token
curl -X GET https://api.leypal.dev/api/v1/webhook-notifications \
-H "Authorization: Bearer sk_live_xxx" \
-H "Content-Type: application/json"Optional query parameters:
| Parameter | Type | Description |
|---|---|---|
event | string | Filter by event type (e.g., signature.created) |
status | string | Filter by delivery status: success, failed, pending |
limit | number | Results per page (default: 50, max: 100) |
offset | number | Pagination offset |
Response fields per delivery record:
| Field | Type | Description |
|---|---|---|
id | string | Notification record ID |
event | string | Event type (e.g., signature.completed) |
webhookId | string | Unique webhook delivery ID (matches X-Leypal-Webhook-Id header) |
organizationId | string | Your organization ID |
status | string | success | failed | pending |
requestBody | object | Exact JSON payload Leypal sent |
responseCode | number | HTTP status code your endpoint returned |
responseBody | string | Response body your endpoint sent |
responseHeaders | object | Response headers your endpoint sent |
deliveredAt | string | ISO 8601 timestamp of delivery attempt |
nextRetryAt | string | ISO 8601 timestamp of next retry (if pending/failed) |
failureReason | string | Why the webhook failed (e.g., "connection timeout") |
attempts | number | Total delivery attempts made so far |
When to use this: If a webhook never arrives, query this endpoint to see whether Leypal attempted delivery at all. If no record exists, the triggering event did not fire. If a record exists, responseCode and failureReason tell you exactly what went wrong.
Troubleshooting Flowchart
Use this flowchart to diagnose webhook delivery problems systematically:
My webhook is not being delivered
│
├─ Step 1: Is the webhook URL registered in your API Key dashboard?
│ ├─ No → Go to API Key dashboard → Webhooks → Add URL → Generate Secret
│ └─ Yes → Continue to Step 2
│
├─ Step 2: Is your endpoint publicly accessible from the internet?
│ ├─ No (localhost/private) → Use ngrok or localtunnel for testing
│ │ Deploy to a public server for production
│ └─ Yes → Continue to Step 3
│
├─ Step 3: Did Leypal attempt delivery?
│ │ Check: GET /api/v1/webhook-notifications?status=failed
│ ├─ No records found → Triggering event did not fire
│ │ Verify you performed the action (e.g., created signature)
│ └─ Records exist → Continue to Step 4
│
├─ Step 4: What does the delivery record show?
│ ├─ responseCode: 2xx → Webhook was delivered; check your own application logs
│ ├─ responseCode: 401 → Signature verification failing
│ │ See: Signature Verification Errors section
│ ├─ responseCode: 4xx → Endpoint rejected request
│ │ See: HTTP Response Codes section
│ ├─ responseCode: 5xx → Endpoint crashed or returned an error
│ │ Check: Your server logs for stack traces
│ └─ responseCode: null → Network error; Leypal could not connect
│ Continue to Step 5
│
├─ Step 5: Can Leypal reach your endpoint?
│ │ Test: curl -X POST https://yourendpoint.com/webhooks -d '{}' -v
│ ├─ curl fails → Fix: firewall rules, DNS, SSL certificate, server running?
│ └─ curl succeeds → Webhook handler is crashing on startup or early in the request
│ Check: Application logs for errors before response is sent
│
├─ Step 6: Is signature verification the problem?
│ │ Check: Is responseCode 401 in the history?
│ ├─ Yes → See: Signature Verification Errors section above
│ └─ No → Check application logs for errors inside the webhook handler
│
└─ Still stuck?
├─ Enable verbose logging in your webhook handler
├─ Check webhook history for exact requestBody and responseBody
├─ Verify your endpoint with: curl -X POST <url> -H "Content-Type: application/json" -d '{"test":true}'
└─ Contact Leypal support with:
- Event type that is not delivering
- Your Organization ID
- Webhook ID (from history API)
- Timestamp of expected delivery
- Server logs showing what your endpoint receivedCommon Scenarios & Solutions
| Scenario | What You See | Root Cause | Solution |
|---|---|---|---|
| Endpoint returns 200 but webhook shows as failed | History: status: failed, responseCode: 200 | Endpoint took >30 seconds to respond before returning 200 | Return 202 in under 1 second; move all business logic to a background queue |
| "Invalid signature" even though secret is correct | Every webhook returns 401 Unauthorized | Raw body not preserved; or string equality used | Add express.raw() before JSON middleware; use crypto.timingSafeEqual() |
| Webhook worked yesterday, stopped today | No webhooks delivered since a specific time | Endpoint went down, or webhook secret was rotated | Check if endpoint is running; compare secret in .env vs dashboard |
| Test webhook succeeds but real webhooks fail | Manual test: delivered; real event: failed | Different endpoint URL (dev vs prod) or firewall difference | Verify production webhook URL is registered; check production firewall |
| Getting 404 on webhook delivery | History: responseCode: 404 | Webhook path doesn't exist on server | Verify URL path in dashboard; check server routing config |
| Leypal keeps showing pending retries after fix | History: status: pending even after endpoint is healthy | Pending retries are on a schedule — they will execute at nextRetryAt | Wait for the next retry; or trigger a new event to generate a fresh webhook |
| Webhook delivered but event type not handled | Application silently ignores event | Switch/if block doesn't cover the event type | Log all incoming event types; add a handler for the missing event |
| Same webhook delivered twice | Duplicate entries in your database | Retry succeeded; or network delivered twice | Implement idempotency: check webhookId before processing, skip duplicates |
Production Checklist
Before deploying your webhook handler to production, verify each item:
- Endpoint uses HTTPS (not HTTP)
- Production webhook URL is updated in Leypal API Key dashboard (not dev/staging URL)
- Webhook secret is stored in environment variable or secrets manager (not hardcoded)
-
.envfile is NOT committed to git (add to.gitignore) - Using
crypto.timingSafeEqual()for signature comparison (not===) - Using
express.raw({ type: 'application/json' })middleware before JSON parsing - Endpoint returns
200or202in under 1 second - Business logic is processed asynchronously (queue system or background job)
-
webhookIdis logged for every incoming event (enables debugging) - Webhook delivery history is monitored periodically (
GET /api/v1/webhook-notifications) - Duplicate delivery is handled via idempotency check on
webhookId - Error handling is in place — unhandled exceptions return
500cleanly, not crash the process
Getting Help
If you have worked through the troubleshooting flowchart and the problem persists, gather the following before contacting support:
- Event type that is not delivering (e.g.,
signature.completed) - Organization ID (visible in your dashboard)
- Webhook ID from the delivery history API (
webhookIdfield) - Timestamp of the expected delivery
- Server logs from the time of the delivery attempt
- Webhook history entry showing
requestBody,responseCode, andfailureReason
Related Guides
| Guide | What It Covers |
|---|---|
| Webhook Setup | Registering endpoints, generating secrets, managing webhook configuration |
| Webhook Signature Verification | Complete HMAC-SHA256 implementation with code examples |
| Webhook Events | Full payload structure for all event types |