Skip to main content
This page covers the most common problems developers encounter when integrating Bondify, along with the steps to diagnose and fix each one. Work through the relevant section for your symptom. If you are still stuck after trying these steps, use the support card at the bottom of the page.

Proof verification fails

Your call to verifyProof() is throwing an error even though the client reported a successful login.
The most common cause. Each Bondify project has its own secret key, and a proof signed for Project A cannot be verified with Project B’s key.Fix: Open the dashboard, navigate to the project your frontend is using, and copy the secret key. Compare the last few characters with what is stored in your BONDIFY_SECRET_KEY environment variable. They must match exactly.
The proof is a signed JWT. If you parse it, re-stringify it, trim it, or pass it through any transformation before calling verifyProof(), the signature will not match.Fix: Pass the proof string directly from the SDK callback to your API request body, and read it back verbatim in your API handler. Do not JSON-parse and re-encode it.
// ✅ Correct — pass through untouched
const { proof } = await req.json();
const user = await verifyProof(proof, process.env.BONDIFY_SECRET_KEY);

// ❌ Wrong — modifying the value before verification
const { proof } = await req.json();
const decoded = JSON.parse(atob(proof.split('.')[1])); // breaks the JWT
If you rotated your secret key in the dashboard but have not yet redeployed your server with the updated BONDIFY_SECRET_KEY, proofs signed after the rotation will fail verification.Fix: Ensure your server environment variable reflects the current secret key shown in the dashboard. Redeploy after updating the variable.
In Next.js, any environment variable prefixed with NEXT_PUBLIC_ is bundled into the browser. If your secret key is accidentally exposed this way, it creates a security vulnerability — and if someone used the leaked key to construct a fake proof, verification behaviour becomes unpredictable.Fix: Ensure your secret key environment variable has no NEXT_PUBLIC_ prefix:
# ✅ Correct — server-only
BONDIFY_SECRET_KEY=sk_live_xxxx

# ❌ Wrong — exposed to browser
NEXT_PUBLIC_BONDIFY_SECRET_KEY=sk_live_xxxx

Session is stuck on pending

Your polling loop keeps receiving pending status and never transitions to confirmed, cancelled, or expired.
The most likely cause. The Telegram deeplink was generated, but the user dismissed the new tab or the popup was blocked before Telegram opened.Fix: Ensure your app clearly communicates to the user that they need to switch to Telegram and tap Confirm. A visible status message like “Open Telegram and tap Confirm to continue” reduces drop-off significantly.
If a user has previously blocked the Bondify Telegram bot, the bot cannot send them the confirmation message, and the session will remain pending until it expires.Fix: Ask the user to search for the Bondify bot in Telegram and unblock it, then try signing in again. You can detect this scenario by a high expired rate in your analytics for specific users.
If the projectId you pass to the SDK does not match an active project in your Bondify account, session creation may succeed but confirmation events will never arrive.Fix: Confirm the projectId in your SDK configuration matches the Project ID (not the secret key) shown in your dashboard.
If the SDK cannot reach the Bondify API (e.g. due to a CORS misconfiguration, an ad blocker, or a network issue), session creation may fail silently.Fix: Open the browser console and the Network tab. Look for failed requests to the Bondify API. Check that apiBase is not overridden to an incorrect value in your SDK configuration.

Telegram popup is being blocked

The browser blocks the window that should open Telegram, and the user sees nothing happen after clicking the button.
Browsers block popups that are not triggered directly by a user interaction (a click, keydown, etc.). If you call signIn() from a useEffect, a setTimeout, a route change handler, or any other async context, the popup will be blocked.Fix: Call signIn() directly inside a click event handler, with no await between the click and the call:
// ✅ Correct — called directly in onClick
<button onClick={() => client.signIn().then(handleSuccess)}>
  Sign in
</button>

// ❌ Wrong — called from useEffect
useEffect(() => {
  client.signIn(); // popup will be blocked
}, []);
If you need to do async work before calling signIn() (e.g. checking if a user already has a session), do that work first and store the result in state. Then call signIn() only when the user clicks the button.

Webhooks not being received

Your webhook endpoint is configured in the dashboard but you are not receiving POST requests after authentications complete.
Bondify’s servers must be able to reach your webhook endpoint over the internet. A localhost URL or an internal network address will never receive deliveries.Fix: Use a tunnelling tool like ngrok during local development to expose your endpoint. In production, ensure the URL is a publicly accessible HTTPS endpoint.
Bondify considers a webhook delivery failed if your endpoint does not respond with an HTTP 2xx status code within 10 seconds. Long-running synchronous processing in the handler will cause this.Fix: Respond with 200 OK immediately on receipt, then process the payload asynchronously (e.g. push it to a queue or background job).
app.post('/webhooks/bondify', (req, res) => {
  res.status(200).send('ok'); // respond immediately
  processWebhookAsync(req.body); // handle in background
});
If you validate the X-Bondify-Signature header (recommended) but have a bug in your verification logic, you may be returning 4xx and discarding valid deliveries.Fix: Temporarily log the raw X-Bondify-Signature header and the raw request body (before any JSON parsing) and compare the expected vs received HMAC. Ensure you are using the raw request body bytes, not a parsed object, when computing the HMAC.
Starter plan webhooks do not retry on failure. If your endpoint returns a non-2xx response or times out, that delivery is lost. Upgrade to the Pro plan for priority webhooks with automatic retry.

High expired rate in analytics

Your analytics dashboard shows a large proportion of sessions ending in expired rather than confirmed.
If users start the auth flow but do not tap Confirm in Telegram within 10 minutes, the session expires. A high expired rate often means your UI is not clearly guiding users to the next step.Fix: Add a prominent status message after the deeplink opens — for example: “We’ve sent a message to your Telegram. Open the app and tap Confirm.” Consider also adding a countdown timer or a Resend button that creates a new session.
Desktop users who do not have the Telegram desktop app installed may see the deeplink open in a browser tab that does nothing.Fix: After opening the deeplink, show a fallback message with a direct link to download Telegram or instructions to scan a QR code on mobile.

cancelled status not handled

Your polling loop continues running after the user cancels, causing unnecessary API calls or a stuck loading state.
If you implement your own polling loop against the REST API (rather than using the SDK), you must check for the cancelled status and exit the loop when you receive it.Fix: Add an explicit exit condition for cancelled:
while (true) {
  const data = await pollStatus(sessionToken);

  if (data.status === 'confirmed') { handleSuccess(data); break; }
  if (data.status === 'expired')   { handleExpired(); break; }
  if (data.status === 'cancelled') { handleCancelled(); break; } // ← required
  if (data.status === 'pending')   { await sleep(2000); continue; }
}
The SDK handles this automatically — this only applies if you are using the raw API.

Still stuck? Contact support

If none of the steps above resolved your issue, reach out to the Bondify support team. Pro and Business plan users receive email responses within 12 hours. Include your project ID, the affected SDK version, and any relevant browser console or server logs.