Troubleshooting
On this page
- Authentication
- 401 unauthenticated
- 403 forbidden
- API requests
- 400 validation_failed
- 404 not_found
- 402, insufficient credits
- 429 rate_limited
- 5xx errors
- Webhooks
- Webhook handler never receives events
- Webhook deliveries fail with 400
- Webhook deliveries fail with 5xx
- Duplicate events
- SDK
- Cannot find module '@selfxyz/enterprise-sdk'
- Webhook verification throws WebhookVerificationError
- Types don’t narrow on event.type
- Mock passports
- When all else fails
- Related
Things that commonly go wrong, and how to unstick them. Errors from the SDK arrive as SelfApiError (err.statusCode, err.code); the sections below are keyed by those.
If none of these apply, email support@self.xyz with your org ID (from the dashboard URL) and, for a webhook issue, the verification_id from the event payload.
Authentication
401 unauthenticated
The API key is missing, malformed, or revoked.
- Confirm the
apiKeyyou pass toSelfClientis set and is the full value (it starts withsk_test_orsk_live_). - If it works locally but 401s in deploy, you’re probably reading the wrong env var. If it worked before and suddenly 401s, the key was likely revoked.
- The full key is shown once on creation. If you’ve lost it, generate a new one under Developer → API keys and revoke the old one.
403 forbidden
The key is recognized but blocked at the key layer (for example a disabled key). Generate a fresh key under Developer → API keys and retire the old one.
Note that a flowId belonging to another organization returns 404 not_found, not 403: lookups are scoped to the key’s org, so a foreign ID is indistinguishable from a nonexistent one. And there’s no test-vs-live mismatch to debug at the flow level, the environment comes from the key itself (sk_test_ vs sk_live_), and a flow is the same ID on both tabs.
API requests
400 validation_failed
The request body didn’t match the schema. The details.issues array tells you what.
{
"error": {
"code": "validation_failed",
"details": {
"issues": [
{ "path": ["externalUuid"], "message": "String must contain at least 1 character(s)" }
]
}
}
}
Fix the field at path. Don’t retry on 400, the request will keep failing.
404 not_found
The flowId (or session ID) is wrong, points at a config that was never deployed, or the flow is archived. A flow that exists but has no deployed version returns 409 conflict instead.
- Open the flow in the dashboard. If you don’t see a “Deployed” badge, click Deploy (otherwise you’ll get
409 conflict). - If the flow was deleted, recreate it; the ID is gone.
For an on-chain flow, a 409 conflict with details.discriminator: 'no_deployment' means the flow’s contract isn’t deployed for that environment yet. Deploys finish in about a minute; check the status on the flow’s Test or Live tab and retry a failed deploy from there.
402, insufficient credits
Your org’s credit balance is too low to cover this session’s cost, so the credit gate blocked it. (The error envelope carries code: "unauthenticated" with HTTP 402, branch on the status, not the code.)
- Check your balance on the Settings → Usage & Billing tab.
- On Free, the signup grant never renews. Once it’s spent, upgrade to Starter. On Starter, the allotment resets each billing cycle; upgrade your plan (or talk to sales about Enterprise) for more capacity.
- Watch the credits meter on that tab so this doesn’t surprise you.
429 rate_limited
Per-API-key rate limit exceeded. Honor the Retry-After header.
- If your traffic legitimately exceeds the limit, contact sales@self.xyz about higher limits or move to Enterprise.
- The SDK doesn’t retry, your code should back off and retry on
429.
5xx errors
Transient. Retry with exponential backoff. If it’s persistent (more than 30 seconds), check status.self.xyz and contact support with the request ID.
Webhooks
Webhook handler never receives events
- Confirm the endpoint is registered under Developer → Webhooks for the same environment as the key creating sessions (test vs live).
- Confirm your endpoint is reachable from the public internet (not behind a VPN, no firewall blocking POST).
- If using a tunnel (ngrok, Cloudflare Tunnel), confirm the tunnel is still up.
- The delivered event is
verification.completed; branch onevent.typeso a future event type doesn’t surprise your handler.
Webhook deliveries fail with 400
Your handler is rejecting the delivery. Most likely:
- Signature verification failed. Check that:
- You’re using the raw body, not a JSON-parsed object.
- The signing secret matches the dashboard’s
whsec_...for this endpoint. - You’re not rewriting headers between proxy and handler.
- Body schema mismatch. Update to the latest SDK version, we may have added an event type your version doesn’t know.
Webhook deliveries fail with 5xx
Your handler is erroring before completing. Find the matching invocation in your logs by the verification_id from the event payload. Self retries 5xx, so once you fix the handler the next retry should land.
Duplicate events
Expected. We deliver at-least-once. See Best practices for dedup patterns.
SDK
Cannot find module '@selfxyz/enterprise-sdk'
The package is ESM-only and requires Node 20+.
- Confirm Node version (
node -v). - Confirm your
package.jsonhas"type": "module", or you’re using.mjsfiles, or your build system supports ESM. - TypeScript:
"module": "NodeNext"or"ESNext"intsconfig.json.
Webhook verification throws WebhookVerificationError
The body or headers were modified between Self and your handler. The usual culprit is JSON-parsing the body before verification: the raw bytes we signed are gone. In a Next.js route handler, read the body with await req.text() and never call req.json() before verify(...). For the full list (proxy rewrites, wrong secret, clock skew), see Verify webhooks.
Types don’t narrow on event.type
if (event.type === 'verification.completed') {
event.status; // TS error?
}
- Confirm you’re importing
WebhookEventfrom@selfxyz/enterprise-sdk(which is the discriminated union), not constructing it yourself. - If you’re using older TypeScript (<4.4), narrowing on discriminated unions may need explicit type guards.
Mock passports
In a test environment (sessions created with an sk_test_ key) you verify with a mock passport from the Self app instead of a real document. See Using mock passports for the setup. Mock credentials are cryptographically distinct from real ones and never verify against a live flow.
When all else fails
Email support@self.xyz with:
- Your Email.
- The
err.codeanderr.messagefrom theSelfApiError, or theverification_idfrom the event for a webhook issue. - What you expected vs. what happened, with approximate timestamps.
The more of that you include, the faster we can trace it.
Related
Thanks — what went wrong?
Thanks for your feedback!