User login
Updated: Sep 4, 2026
Copy for LLM
Verifying someone is a single moment. Being logged in is a state that persists, expires, and eventually has to end. A conversation that never establishes a session cannot personalize anything; one that never ends a session shows the second person to use a shared phone the first person’s order history. This page covers the session itself: starting anonymous, upgrading to a signed-in customer, deciding what that unlocks, renewing it before it lapses, and tearing it down at the right moment.
The agent handles:
- Starting work as an anonymous guest so browsing needs no sign-in
- Upgrading a guest to a signed-in customer, carrying their work across
- Recognizing a returning customer without asking them to sign in again
- Renewing a session that is about to expire, without interrupting the conversation
- Gating each capability on the right level of identification, not all on one flag
- Ending the session at completion, on request, or on inactivity
Who this guide is for
The examples use a train operator, but the pattern fits any business where some things need an identity and others do not. If everything needs sign-in, the session is trivial. If nothing does, you have no session. The interesting case is the mixture, which is most businesses.
| Organization type | Available to a guest | Requires a signed-in customer |
|---|---|---|
Rail and coach operators | Timetables, fares, live departures | Existing bookings, changes, refunds, saved railcards |
Gyms and fitness clubs | Class timetables, joining options, site facilities | Bookings, membership status, attendance history |
Retail with accounts | Browsing, stock, prices, returns policy | Saved payment methods, loyalty balance, past orders |
Events and ticketing | Event listings, venue information, seat maps | Your tickets, transfers, refunds |
Membership organizations | Public events, joining information | Renewal status, member benefits, personal details |
This guide is about the session. Establishing the identity that starts it is in Customer OTP authentication, and this page assumes you have read it: the connector configuration, the
auth tool, and the token extraction described there are the foundation for everything below.Each step below notes how the API is used in other scenarios, then applies it to the rail example.
How a session works
Meta Business Agent holds the customer credential for you. When a tool declares
user_auth_action_config with user_action_tool_type set to auth, the token in that tool’s response is captured and stored against the conversation. Every later tool with user_auth_required set to true has that token injected at the location the connector’s user_auth_injection_config names.Three consequences follow, and they shape the rest of this page.
The session is scoped to the conversation. It is not scoped to the phone number, and it does not survive into a different conversation.
The agent never handles the session credential. It is captured from a response and injected into a request without passing through anything the agent composes, which is why no agent instruction should ever mention the session token by name. Distinguish it from the short-lived verification artifact your passcode check returns: that one is an ordinary tool parameter, so the agent does carry it from the verification response into the
auth tool that exchanges it for a session. Keep the artifact good for a single exchange — it is the only value in this flow that passes through the conversation.Session state is not conversation memory. The credential persists because the platform stores it. Everything else — which account, how far through a task, what has been confirmed — is only as durable as your own systems make it.
Prerequisites
- An onboarded and enabled Meta Business Agent (
rollout.enabled: true) — see End-to-end setup - A valid system user access token (SUAT) or Business Integration System User (BISU) token with the
whatsapp_business_messagingpermission — see Generate an access token - Your WhatsApp Business phone number ID (
{entity_id}) — find it in WhatsApp Manager under Account tools > Phone numbers - A connector configured with
user_auth_injection_config, and anauthtool that returns a customer token — see Customer OTP authentication - An endpoint that issues a guest identifier, required by Start as a guest
- An endpoint that merges guest work into a customer account on sign-in, required by Upgrade a guest to a customer
- An endpoint that invalidates a session, required by End the session
- A decision, written down, about which capabilities need which level of identification
Start as a guest
Do not open with a sign-in request. Most conversations never need one, and asking first is the fastest way to lose the ones that would have converted. Issue a guest identifier instead, so work done before sign-in has somewhere to live.
curl -X POST "https://api.facebook.com/{entity_id}/agent_connectors/{connector_id}/tools" \
-H "Authorization: Bearer {ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-H "X-API-Version: 2.0.0" \
-d '{
"name": "start_guest_session",
"description": "Creates an anonymous guest session and returns a guest identifier for holding work before the customer signs in. Call this once, the first time the conversation needs to record anything. Do not call it again in the same conversation, and do not call it after the customer has signed in.",
"user_auth_required": false,
"request_definition": {
"method": "POST",
"path": "/sessions/guest",
"body": {
"content_type": "application/json",
"params": {
"conversation_id": {
"type": "string",
"description": "The conversation this guest session belongs to.",
"binding": {
"kind": "macro",
"macro": "WHATSAPP_CONVERSATION_ID"
}
}
},
"required": ["conversation_id"]
}
}
}'
“Call this once” is doing real work in that description. An agent that re-bootstraps a guest session mid-conversation abandons whatever the previous one held, and the symptom is a basket or a form that empties for no visible reason.
Bind the guest session to the conversation rather than to the phone number. A phone-scoped guest session is shared by everyone who uses that handset, which is exactly the leak the session is supposed to prevent.
Where else this applies. The guest-first pattern suits any flow with a meaningful anonymous phase: browsing, quoting, checking eligibility, filling in most of a form. Where every action needs an identity, skip this and verify at the start.
Recognize a returning customer
Some customers you already know. Bind the WhatsApp phone number and look them up before deciding whether a sign-in is needed at all.
curl -X POST "https://api.facebook.com/{entity_id}/agent_connectors/{connector_id}/tools" \
-H "Authorization: Bearer {ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-H "X-API-Version: 2.0.0" \
-d '{
"name": "lookup_customer_by_phone",
"description": "Checks whether this WhatsApp number is registered to a customer account and returns their first name and whether the account is active. Returns no personal data beyond the first name. Call this when the customer asks for something that needs an account, to decide whether to offer sign-in. A match here is not a sign-in and must never be treated as one.",
"user_auth_required": false,
"request_definition": {
"method": "GET",
"path": "/customers/lookup",
"query_parameters": {
"phone": {
"type": "string",
"description": "The WhatsApp phone number of the person messaging.",
"required": true,
"binding": {
"kind": "macro",
"macro": "WHATSAPP_PHONE_NUMBER"
}
}
}
}
}'
A match tells you the number is registered. It does not tell you who is holding the phone. Use it to make sign-in shorter — “Welcome back, Sam. Shall I send a code to finish signing you in?” — and never to skip sign-in entirely.
Return as little as possible. A first name personalizes the greeting; anything more is disclosed to whoever picked up the handset, before any verification has happened.
Where else this applies. Recognition-without-authentication is a useful middle state in most consumer flows, and a dangerous one if the boundary blurs. Keep the lookup deliberately thin so that even a full compromise of this call discloses nothing worth having.
Upgrade a guest to a customer
When the conversation reaches something that needs an identity, sign the customer in and carry their guest work across. The
auth tool from Customer OTP authentication does the verification; the merge is what makes it feel continuous.Pass the guest identifier into the sign-in so your service can attach the work to the account.
verification_token here is the verification artifact, not the session credential: the agent supplies it from the passcode check, and the session token your endpoint returns is what the platform captures at user_auth_token_path and injects from then on.curl -X POST "https://api.facebook.com/{entity_id}/agent_connectors/{connector_id}/tools" \
-H "Authorization: Bearer {ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-H "X-API-Version: 2.0.0" \
-d '{
"name": "sign_in_customer",
"description": "Completes sign-in for a verified customer and returns a session token. Pass the guest identifier from start_guest_session so anything collected before sign-in is attached to the account. Call this only after the passcode has been verified.",
"user_auth_required": false,
"user_auth_action_config": {
"user_action_tool_type": "auth",
"user_auth_token_path": "session.access_token",
"refresh_token_path": "session.refresh_token",
"expires_at_path": "session.expires_in",
"expires_at_type": "relative_seconds"
},
"request_definition": {
"method": "POST",
"path": "/sessions/customer",
"body": {
"content_type": "application/json",
"params": {
"verification_token": {
"type": "string",
"description": "The short-lived token returned by passcode verification."
},
"guest_id": {
"type": "string",
"description": "The guest identifier from start_guest_session, when this conversation had one. Omit it if the customer signed in before doing anything as a guest."
},
"conversation_id": {
"type": "string",
"description": "The conversation this session belongs to.",
"binding": {
"kind": "macro",
"macro": "WHATSAPP_CONVERSATION_ID"
}
}
},
"required": ["verification_token", "conversation_id"]
}
}
}'
Do the merge in your own service, not in the conversation. Given the choice, an agent will try to re-do the guest’s work under the new session — re-adding items, re-entering an address — and it will get some of it wrong. One call that transfers ownership server-side is both faster and correct.
Decide what happens when the guest work conflicts with what is already on the account. A saved address and a guest-entered address are both plausible; picking silently is how customers end up with deliveries to a house they moved out of. Prefer the value entered in this conversation, and say which one you used.
Where else this applies. Guest-to-customer merges appear anywhere anonymous work precedes identification. The rule that generalizes is that the merge is a server-side operation with a defined conflict policy, never a sequence of steps the agent replays.
Gate capabilities by level
A single signed-in flag is too blunt for most businesses. Set the level per capability, and set it in the tool rather than only in an agent instruction — the tool boundary is enforced, the instruction is guidance.
| Level | How it is established | Suitable for |
|---|---|---|
Anonymous | No identification at all | Product information, hours, policies, stock |
Guest | start_guest_session | A basket, a quote, a partially completed form |
Recognized | lookup_customer_by_phone matched | A personalized greeting, and nothing else |
Signed in | Passcode verified, auth tool succeeded | Order history, saved details, most account actions |
Re-verified | A second, fresh verification | Changing a payment method, address, or contact details |
The last row is the one most often skipped. A conversation that has been open for twenty minutes has a signed-in session whose verification is twenty minutes old, and a phone that changes hands in that window carries the session with it. For anything whose consequences are hard to reverse, verify again at the point of action rather than relying on a session established earlier.
Express the gate as
user_auth_required on the tool. A capability protected only by a sentence in an agent instruction will eventually be reached by a conversation that talked its way around the sentence.Where else this applies. Any business that mixes low-risk and high-risk actions needs a ladder rather than a switch — a retailer separating browsing from saved cards, a utility separating tariff information from payment details, a ticketing platform separating listings from transfers. The platform itself gives you one boolean per tool, so the ladder lives in your design and your backend rather than in a field you can set. Write yours down before you configure anything: adding a rung later means revisiting every tool you have already shipped.
Keep the session alive
Conversations pause. Someone replies twenty minutes later, and a token issued for fifteen has expired. With a
refresh tool configured, Meta Business Agent renews the session without involving the customer; without one, they are asked to verify again, which reads as the agent having forgotten them.Configure the refresh tool as described in Customer OTP authentication. Two details matter for the session specifically.
Refresh tokens should rotate. Issue a new refresh token with each renewal and invalidate the previous one, so a captured token has a short useful life.
Refreshing is not re-verifying. A refreshed session is as old as its original verification, however many times it has been renewed. Keep the re-verification gate on high-risk actions tied to when the customer last proved who they were, not to when the token was last renewed.
Set the token lifetime deliberately. Long tokens keep conversations smooth and extend the window in which a shared phone is a problem. Short tokens do the opposite. For most consumer flows fifteen to thirty minutes of inactivity is a reasonable balance, with high-risk actions gated separately.
End the session
Sessions need to end, and the moments are predictable.
| Trigger | What should happen |
|---|---|
The task completes | Invalidate the session once the order, change, or request is finished |
The customer asks | Treat “log me out” as an instruction, and confirm it is done |
Inactivity | Expire after a defined idle period rather than at the end of the day |
Handoff to a person | Invalidate before transferring, so the colleague verifies independently |
A different account is requested | Invalidate rather than switching accounts within a session |
curl -X POST "https://api.facebook.com/{entity_id}/agent_connectors/{connector_id}/tools" \
-H "Authorization: Bearer {ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-H "X-API-Version: 2.0.0" \
-d '{
"name": "end_session",
"description": "Ends the signed-in session and invalidates its tokens. Call this when the customer asks to be logged out, when the task they signed in for is complete, and before handing the conversation to a colleague. Confirm to the customer that they are signed out.",
"user_auth_required": true,
"request_definition": {
"method": "POST",
"path": "/sessions/end"
}
}'
The completion case is the one to get right. A booking conversation that ends with a ticket bought should not leave a session open on a phone that goes back in a pocket. End it, say so, and let the next request start again.
Never let one session serve two accounts. “Can you check my partner’s booking too?” is a reasonable-sounding request and a hard no: end the session, and start again as that person if they are present.
After ending a session, drop everything derived from it. A conversation that has signed out but still refers to the previous customer’s order has not really signed out, and that is what the customer sees.
Where else this applies. Explicit teardown matters more on a shared, persistent channel than in a browser, where closing a tab does much of the work. In a WhatsApp thread nothing closes, so if you do not end the session, nothing does.
Test the agent
Use Agent test to simulate conversations before going live. Pass
conversation_id to continue a multi-turn exchange.curl -X POST "https://api.facebook.com/{entity_id}/agent_test" \
-H "Authorization: Bearer {ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-H "X-API-Version: 2.0.0" \
-d '{
"user_msg": "Can you show me my last booking?"
}'
Cover these scenarios in your test plan:
| Scenario | What it verifies |
|---|---|
Anonymous question, no sign-in offered | The agent does not gate what it need not gate |
Guest work then sign-in | Guest content survives the merge |
Guest address conflicts with saved address | The conflict policy runs and is stated |
Returning number recognized | Greeting personalizes without disclosing |
Recognized but never verified | No account data reaches the conversation |
Token expires mid-conversation | Refresh runs without re-verification |
High-risk action on an old session | Re-verification is demanded |
Explicit sign-out | Session ends and is confirmed |
Question after sign-out | Nothing from the previous session survives |
Second account requested | Refused, session ended |
Handoff while signed in | Session invalidated before transfer |
Two sign-ins in one conversation | The second does not inherit the first |
The last six deserve automated regressions. Every one of them is a case where the agent behaves reasonably and the session boundary is what fails.
Where else this applies. Session bugs hide in transitions rather than in steady states. Test the moments where state changes hands — merge, refresh, sign-out, handoff — far harder than the signed-in path itself.
Example conversation
This exchange shows the configuration above in use: anonymous browsing, a guest session, an upgrade that carries the held seat, a re-verification for a high-risk change, and a clean teardown.
Turns annotated with (calls ...) use the four tools defined on this page. Holding a seat, booking the ticket, and changing the saved card are not configured here — those belong to your reservation and payment systems, and are shown only so the session transitions read as a whole conversation.
Customer: What time is the last train to Leeds on Friday evening?
Agent: The last direct service leaves at 21:38 and arrives at 23:52. There are earlier ones at 18:05 and 19:20 if you would rather not travel that late.
Customer: Hold me a seat on the 18:05
Agent:(calls
start_guest_session)Held — one seat on the 18:05 to Leeds, coach C, seat 41. That is £42.50 as a standard single.
Customer: Do I have a railcard saved? It should be cheaper than that.
Agent:(calls
lookup_customer_by_phone)Welcome back, Sam. Railcard discounts need you signed in before I can check for one — shall I send a code to the mobile on your account?
Customer: Yes
Agent:(verification runs, then calls
sign_in_customer)Signed in, and your seat on the 18:05 is still held. Your Two Together Railcard is valid until March, which brings the single down to £28.35.
Customer: Book it on my saved card — but use the new one ending 4471
Agent: I can book it now. Changing the card on your account needs one more check, so I’ll send another code first.
(re-verification runs)
Card updated, and your ticket is booked — coach C, seat 41 on the 18:05 to Leeds this Friday.
Customer: Thanks, that’s everything
Agent:(calls
end_session)All done — I’ve signed you out. Message any time if you need anything else.
Monitor connector health
The connector logs endpoint,
GET /{entity_id}/agent_connectors/{connector_id}/logs, reports how your session endpoints are answering.curl -X GET "https://api.facebook.com/{entity_id}/agent_connectors/{connector_id}/logs?summary_only=true&top_n=5&include_stats=true" \
-H "Authorization: Bearer {ACCESS_TOKEN}" \
-H "X-API-Version: 2.0.0"
Logs cover the last seven days, and the requested time range cannot exceed seven days. The response carries two objects:
| Object | Field | What it reports |
|---|---|---|
data | failure_code_name | The failure category |
data | error_message | The message returned by your endpoint |
data | tool_name | Which tool made the call |
data | occurrences | How many times this failure appeared |
stats | success_rate | Share of calls that succeeded |
stats | avg_latency_s | Mean round-trip time in seconds |
stats | p95_latency_s | 95th percentile round-trip time in seconds |
stats | p99_latency_s | 99th percentile round-trip time in seconds |
Three signals are specific to sessions and none of them are visible in the overall success rate.
Authentication failures on tools with
user_auth_required set to true. A cluster of these usually means an expiry misconfiguration rather than genuinely expired sessions.start_guest_session called more than once per conversation. Each extra call is work silently discarded.Sign-ins without a matching
end_session. A persistent gap means sessions are being left open, which is the failure with the longest tail.Where else this applies. Session health is a ratio problem rather than an error-rate problem. The counts that matter are between tools — guests per conversation, sign-ins against sign-outs, refreshes against verifications — and a connector dashboard showing only success rates will look healthy throughout.
Tips for this use case
These are the session decisions that most often go wrong once the agent is live.
Do not ask for sign-in up front. Most conversations never need it, and asking first costs you the ones that would have gone somewhere.
Scope guest sessions to the conversation. A phone-scoped guest session is shared by everyone who uses the handset.
Bootstrap the guest session exactly once. A second call quietly discards whatever the first one held.
Recognition is not authentication. A matched phone number shortens sign-in. It never replaces it.
Return only a first name from the lookup. Anything more is disclosed before verification to whoever is holding the phone.
Merge server-side with a stated conflict policy. Replaying guest work through the agent produces near-misses, and silent conflict resolution produces deliveries to old addresses.
Use levels, not a boolean. Anonymous, guest, recognized, signed in, and re-verified are five different things, and collapsing them either blocks harmless requests or permits harmful ones.
Re-verify for irreversible actions. A session established twenty minutes ago is not evidence about who is holding the phone now.
Rotate refresh tokens, and remember refreshing is not re-verifying. A renewed session is as old as its original verification.
End sessions on completion, not just on request. Most customers never ask to sign out, and a session left open on a shared phone is the failure with the longest tail.
Never switch accounts inside a session. End it and start again.
Summary checklist
Confirm each of the following before you enable the agent on a production phone number.
- Wrote down which capabilities need anonymous, guest, recognized, signed-in, or re-verified status
- Confirmed anonymous questions are answered without any sign-in prompt
- Defined
start_guest_sessionscoped toWHATSAPP_CONVERSATION_ID, not to the phone number - Instructed the agent to bootstrap a guest session exactly once per conversation
- Limited
lookup_customer_by_phoneto a first name and an active flag - Confirmed a phone-number match never unlocks account data on its own
- Passed
guest_idinto sign-in and performed the merge server-side - Defined and documented the conflict policy for guest data against saved data
- Set
user_auth_requiredtotrueon every tool that returns account data - Added a re-verification gate on payment, address, and contact detail changes
- Configured a
refreshtool and confirmed refresh tokens rotate - Confirmed the re-verification gate keys off last verification, not last refresh
- Set a token lifetime appropriate to the sensitivity of the data
- Defined
end_sessionand called it on completion, on request, and before handoff - Confirmed nothing from a previous session survives sign-out
- Confirmed an account switch inside a session is refused
- Tested merge, expiry, refresh, sign-out, handoff, and repeated sign-in
- Monitored guests per conversation and sign-ins against sign-outs, not only success rates