Server Validation
Browser success is not login complete. Your backend must call POST /verify/validate — single-use — before creating your application session.
On this page
Trust boundary
BROWSER YOUR BACKEND KEYRA USER
│ │ │ │
│ Start (publishable cp_*) │ │ │
├────────────────────────────────────────────────►│ │
│ authorize_url │ │ │
│◄────────────────────────────────────────────────┤ │
│ Open popup/redirect │ │ │
│ │ │◄─── verify ────────┤
│ authorization code │ │ │
│◄────────────────────────────────────────────────┤ │
│ Token exchange (PKCE) │ │ │
├────────────────────────────────────────────────►│ │
│ access_token │ │ │
│ (= verification_token) │ │ │
│◄────────────────────────────────────────────────┤ │
│ POST token to backend │ │ │
├─────────────────────────►│ │ │
│ │ POST /verify/validate│ │
│ ├─────────────────────►│ │
│ │ valid + user │ │
│ │◄─────────────────────┤ │
│ │ Create YOUR session │ │
│ Set app session cookie │ │ │
│◄─────────────────────────┤ │ │- Browser: obtain
access_token/verification_token(same value); POST it to your API. - Server: call validate; map KEYRA user → your user; create your session cookie/JWT.
REST
POST
/verify/validatePOST https://auth.keyra.ie/verify/validate
Content-Type: application/json
{
"verification_token": "…",
"client_id": "cp_test_…"
}| Field | Required | Description |
|---|---|---|
verification_token | Yes | Same value as access_token from token exchange |
client_id | No | Publishable client id; recommended to scope validation |
Server SDK
import { createKeyraServer } from "@keyra/typescript-sdk";
const keyra = createKeyraServer({
baseUrl: process.env.KEYRA_BASE_URL ?? "https://auth.keyra.ie",
});
app.post("/api/auth/keyra", async (req, res) => {
const verification_token = String(req.body.verification_token ?? "");
if (!verification_token) {
return res.status(400).json({ error: "verification_token required" });
}
const outcome = await keyra.validateVerification({
verification_token,
client_id: process.env.KEYRA_PUBLISHABLE_CLIENT_ID,
});
if (!outcome.valid) {
return res.status(401).json({ error: outcome.error ?? "invalid" });
}
// Map outcome.user → YOUR user, then create YOUR session (HttpOnly cookie / etc.)
return res.json({ ok: true });
});Success response
responsePOST /verify/validate (success)
{
"valid": true,
"verification_id": "…",
"client_id": "cp_test_…",
"expires_at": "2026-08-05T12:00:00.000Z",
"user": {
"id": 123,
"phone": "+353…",
"fullName": null,
"email": null,
"role": "developer",
"isAdmin": false,
"accessRole": null
}
}Access token lifetime defaults to ~3600s when unset (clamped 60–604800 via OAUTH_ACCESS_TOKEN_TTL_SECONDS). Validate still consumes the token once regardless of TTL remaining.
Single-use & errors
| Error | HTTP | Meaning |
|---|---|---|
verification_token_already_used | 409 | Replay / double validate |
invalid_verification_token | 401 | Unknown, expired, or malformed token |
invalid_client | 401 | Client id rejected for this token |
Full matrix: Errors.
Create your session
- Only after
valid: truecreate your application session (secure, HttpOnly cookie or equivalent). - Map
user.id(and phone/email if present) onto your account model. - Do not re-validate the same token on every request — issue your session and use that.
Session design: Sessions.
