← All posts
Tutorial

WhatsApp Passkey QR Authentication: Build the Webhook Setup Safely

WhatsApp QR pairing can now include a Passkey identity check. If your QR flow returns passkey_required, do not restart the account or create a second WhatsApp connection. Continue the same authentication session, collect the browser-generated WebAuthn credential response, submit it to UnifyPort, and wait for authorized plus the signed inbound webhook events that follow.

Key takeaways

  • WhatsApp’s own Help Center says a passkey links the account to the phone’s security system, such as fingerprint, face, or screen lock, for identity verification.
  • WhatsApp linked-device flows can use QR code scanning or an 8-character phone-number code; both can ask the user to confirm identity on the primary phone.
  • In UnifyPort, Passkey is a continuation state of the WhatsApp QR flow: passkey_requiredpasskey_pending → optional passkey_confirmationauthorized.
  • Register the webhook before authentication so account.auth.required, account.auth.succeeded, and later message.received events are not missed.
  • If you have not built the receiver yet, start with the webhook-first inbound checklist and keep the HMAC replay protection guide open while testing signatures.

What WhatsApp is asking for

This is not a new messaging API credential. It is an identity step inside WhatsApp account access.

WhatsApp’s official passkey article explains that a passkey uses the device security system and can be used when WhatsApp needs to verify identity. Its linked-device articles also describe QR linking and phone-number linking for WhatsApp Business, including prompts to confirm identity with biometrics or the phone unlock PIN. In other words: when your UnifyPort QR flow reaches Passkey, treat it as a user-facing authentication ceremony, not as a server secret you can manufacture.

That distinction matters operationally. Your backend should hold the UnifyPort account state and webhook receiver. The human account owner should complete the WebAuthn prompt in the browser or on the authorized phone. Your logs should never store the full authorize_url, auth_payload.public_key, or serialized credential response.

The UnifyPort flow

The deepest implementation references are Create provider authorization session and Submit Passkey credential response. The relevant public states are:

StateMeaningWhat to do
awaiting_qr_scanA QR code is activeShow the QR to the account owner and poll GET /v1/accounts/{account_id}/auth or POST /v1/accounts/{account_id}/auth/qr/check.
passkey_requiredWhatsApp requires a WebAuthn credentialOpen the hosted authorize_url and let the owner complete the browser credential prompt.
passkey_pendingThe credential was submittedKeep polling the auth state; do not start a new flow.
passkey_confirmationWhatsApp asks for explicit confirmationCall POST /v1/accounts/{account_id}/auth/passkey-confirm when the owner confirms.
authorizedAuthentication is completeThe runtime normally starts automatically; watch for account.auth.succeeded and account.started.

Step 1: create the webhook first

curl -X POST https://api.unifyport.ai/v1/webhook-endpoints \
  -H "X-Api-Key: $UNIFYPORT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "url": "https://ops.example.com/unifyport/webhook",
  "status": "active",
  "subscribed_events": ["account.auth.required", "account.auth.succeeded", "message.received"],
  "signing_secret": "stored-in-your-secret-manager",
  "retry_policy": { "max_attempts": 3 }
}'

Every production receiver should verify X-Device-Signature against the raw body. The webhook delivery docs define the HMAC-SHA256 input as X-Device-Timestamp + "." + raw request body.

Step 2: create a WhatsApp messaging account

curl -X POST https://api.unifyport.ai/v1/accounts \
  -H "X-Api-Key: $UNIFYPORT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "WhatsApp Support Passkey Test",
  "provider": "whatsapp",
  "region": "global",
  "status": "active",
  "auth_mode": "qrcode",
  "capabilities": ["send_message", "receive_message"],
  "provider_data": { "device_os": "Chrome", "device_platform": "web" },
  "metadata": { "environment": "staging" }
}'

Store the returned account_id. The account object exposes runtime_status; the separate authentication endpoint exposes the auth state and payload.

Step 3: start QR authentication and continue Passkey

For a hosted WhatsApp QR session, call the authorization-session endpoint and open the returned authorize_url for the account owner:

curl -X POST https://api.unifyport.ai/v1/accounts/acc_whatsapp_passkey_test/auth-sessions \
  -H "X-Api-Key: $UNIFYPORT_API_KEY"

If the response or subsequent auth check returns:

{
  "data": {
    "account_id": "acc_whatsapp_passkey_test",
    "provider": "whatsapp",
    "auth_mode": "qrcode",
    "status": "passkey_required",
    "auth_payload": {
      "type": "passkey",
      "public_key": { "challenge": "base64url-challenge-from-session" }
    },
    "authorize_url": "https://auth.example.com/authorize/whatsapp/session-token",
    "expires_at": "2026-09-10T08:10:00Z"
  }
}

open authorize_url in the owner’s browser. The browser creates a WebAuthn credential response. Submit that serialized response through POST /v1/accounts/{account_id}/auth/passkey-response only from your trusted auth handoff layer:

curl -X POST https://api.unifyport.ai/v1/accounts/acc_whatsapp_passkey_test/auth/passkey-response \
  -H "X-Api-Key: $UNIFYPORT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "webauthn_response": "{\"id\":\"credential-from-browser\",\"response\":{\"clientDataJSON\":\"base64url-client-data\"}}"
}'

If the state becomes passkey_confirmation, call:

curl -X POST https://api.unifyport.ai/v1/accounts/acc_whatsapp_passkey_test/auth/passkey-confirm \
  -H "X-Api-Key: $UNIFYPORT_API_KEY"

Then poll GET /v1/accounts/{account_id}/auth until the state is authorized or failed.

Step 4: verify the first inbound event

A successful account eventually produces account events and then normal inbound messages:

{
  "id": "evt_2f9c1a4b7e",
  "type": "message.received",
  "provider": "whatsapp",
  "account_id": "acc_whatsapp_passkey_test",
  "occurred_at": "2026-09-10T08:15:21Z",
  "data": {
    "conversation": { "id": "8613912345678@s.whatsapp.net", "type": "user" },
    "sender": { "id": "8613912345678@s.whatsapp.net", "type": "user", "name": "Jordan Lee" },
    "message": {
      "id": "wamid.HBgM",
      "text": "Can you confirm my order?",
      "direction": "inbound",
      "sent_at": "2026-09-10T08:15:20Z"
    },
    "event": { "kind": "message_received" }
  }
}

At this point the Passkey step is over. Route and store events exactly like any other WhatsApp inbound flow. For broader setup context, compare this article with Connect a Telegram user account to a webhook and the first API key webhook test checklist.

Limitations and trade-offs

Use the official WhatsApp Business Platform when you need approved business identity, templates, official analytics, or Meta’s managed policy surface. Use UnifyPort’s unofficial interface when the operational goal is to connect an existing ordinary WhatsApp inbox to a signed webhook without waiting for official business setup.

Passkey handling does not remove WhatsApp’s user-consent step. It makes that step explicit in your runbook: a user must complete the credential prompt, your server must avoid logging sensitive ceremony data, and the webhook must be ready before the account goes online.

FAQ

Is passkey_required an error?

No. It is a normal continuation state for WhatsApp QR authentication. Continue the same session and submit the browser-generated credential response.

Should I create another WhatsApp account if QR reaches Passkey?

No. Creating another account can create duplicate provider-identity conflicts. Poll the current auth state and continue the Passkey branch.

What should I store?

Store the UnifyPort account_id, auth state, webhook event IDs, and received messages. Do not store full hosted authorization URLs or serialized WebAuthn credential responses in logs.

Which webhook events matter during setup?

Subscribe at least to account.auth.required, account.auth.succeeded, and message.received. Many teams use ["*"] during staging, then narrow the list after validation.

Sources checked on 2026-09-10

UnifyPort API

Turn messaging integration into a stable product pipeline.

Start by sending through one API, then bring every inbound message back into your business system with standard events.