POST /v1/sessions/{session_token}/logged-in
Assert that a session's visitor authenticated in your application. Private-key authenticated, body-less by design, idempotent.
curl -X POST \
https://api.botect.ai/v1/sessions/sess_9f3c.../logged-in \
-H "Authorization: Bearer sk_YOUR_PRIVATE_KEY"
Http::withToken(env('BOTECT_PRIVATE_KEY'))
->post('https://api.botect.ai/v1/sessions/'.$sessionToken.'/logged-in');
await fetch(
`https://api.botect.ai/v1/sessions/${sessionToken}/logged-in`,
{
method: 'POST',
headers: { Authorization: `Bearer ${process.env.BOTECT_PRIVATE_KEY}` },
},
);
import os, requests
requests.post(
f"https://api.botect.ai/v1/sessions/{session_token}/logged-in",
headers={"Authorization": f"Bearer {os.environ['BOTECT_PRIVATE_KEY']}"},
)
{
"logged_in": true,
"asserted_at": "2026-09-01T10:15:00Z"
}
{
"error": "Unknown session",
"message": "No session with that token exists for this project. Sessions are minted when the collector first reports (about half a second after page load) — assert after the SDK has run, and retry if the login raced it.",
"code": "UNKNOWN_SESSION"
}
{
"error": "Invalid payload",
"message": "This endpoint accepts no request body or query parameters. Botect stores a timestamp only — never user identifiers.",
"code": "INVALID_PAYLOAD"
}
Call this from your login handler after a successful sign-in. Authenticated by the private key (secret), like the verdict endpoint, and against the same session token.
POST https://api.botect.ai/v1/sessions/{session_token}/logged-in
Botect records a timestamp and nothing else — see Logged-in visitors for what the assertion buys and how long it lasts.
Authentication
Private key via Authorization: Bearer sk_… (or the X-Botect-Private-Key header). The session must belong to the authenticated project. See Authentication.
Authorization: Bearer sk_YOUR_PRIVATE_KEY
Server-side only. Calling this from the browser would let any script — including the automation you are paying to detect — claim to be signed in.
Path parameters
The session token your SDK obtained from ingest — the same one you pass to the verdict endpoint.
Request body
None. This endpoint rejects any request body and any query string with a 422.
That is the privacy guarantee made structural: there is no field in which a user id, email, or account reference could reach Botect, so none can arrive by mistake. An empty JSON container ({}) is tolerated, since many HTTP clients send one on a body-less POST.
Example
Response fields
Always true on success — the assertion is recorded.
ISO-8601 timestamp of the current assertion, which is what the protection window is measured from. Expiry is this value plus 24 hours, so you can compute it directly from the response.
Idempotency
Repeats are safe and expected — call this on every login, not just the first. Each call restarts the 24-hour window and returns the new asserted_at.
No idempotency key is needed: the operation is a timestamp write, so replaying it is indistinguishable from asserting again.
Errors
| Status | code | When |
|---|---|---|
401 | UNAUTHENTICATED | Missing / bad private key, or scoring not enabled |
402 | NO_ACTIVE_SUBSCRIPTION | Owning account has no active subscription |
404 | UNKNOWN_SESSION | No such session for this project |
422 | INVALID_PAYLOAD | A request body, a query string, or a malformed session token |
Unlike the verdict endpoint — a read that fails open — this is a write, so an assertion that did not land says so with a 404 rather than a cheerful 200. A token belonging to another project returns the same 404 as one that does not exist, so the endpoint never reveals which tokens are real.
A 404 most often means the login beat the collector: the session row is created by the SDK's first report, roughly half a second after page load. Retry once, or assert after the visitor's first navigation.