Developer Docs

Integrate with tenka.auth

Add passwordless, phone-approved login to your application. Your app asks Tenka to authenticate a user; the user approves on their phone (Face ID / fingerprint / PIN) with number matching; your app receives a signed, verifiable result. Your app never handles passwords or passkeys.

How it works Prerequisites Base URL & auth Integration flow Endpoint reference Verifying the result Full example Security

How it works

tenka.auth is a push-approval authenticator (the Duo/Okta-Verify model). It is a factor, not a password store. The exchange is always the same three steps:

  1. Your app calls POST /auth/request with the user's email and your API key.
  2. Tenka pushes a request to the user's registered device(s); the user approves and matches a number.
  3. Your app polls the request until it resolves, then verifies the returned EdDSA-signed JWT against Tenka's public JWKS.

Because the result is a signed token bound to your application, a forged "approved" message is worthless — you verify it cryptographically, offline.

Prerequisites

Base URL & authentication

Base URL:

https://mfa.tenka.cloud/api/v1

Every request except the JWKS endpoint requires your Application API key, sent as a bearer token (x-api-key is also accepted):

Authorization: Bearer ak_your_application_api_key

The API key is a server-side secret. Call this API from your backend only — never from a browser or mobile client, and never commit it to source control.

Integration flow

  1. User initiates login in your app (they identify themselves, e.g. by email).
  2. Your backend calls POST /auth/request with that email.
  3. If number_matching is true, display number_code to the user — their phone will ask them to pick the matching number.
  4. Poll GET /auth/request/:id (or the returned poll_url) every ~2s until status is no longer pending, or until expires_at passes.
  5. On approved, verify result_token against the JWKS (checking issuer, audience, expiry). Then create your app's own session for the user.
  6. On denied / expired / failed, refuse the login.

Endpoint reference

POST /auth/request

Create an authentication request and push it to the user.

Request body (JSON):

FieldRequiredDescription
usernameyesThe user's email address.
reasonnoWhy (shown to the user), e.g. "VPN login".
locationnoWhere from (shown to the user), e.g. "Stockholm, SE".
requesting_devicenoWhat is asking, e.g. "VPN Client" (max 200 chars).

201 Created response:

{
  "request_id": "req_...",
  "status": "pending",
  "number_matching": true,
  "number_code": "042",          // DISPLAY this to the user (null if disabled)
  "expires_at": 1737000000000,   // epoch ms
  "poll_url": "https://mfa.tenka.cloud/api/v1/auth/request/req_..."
}

Errors:

StatuserrorMeaning
400username_requiredNo email supplied.
401missing_api_key / invalid_api_keyBad or absent key.
403application_disabledThe Application is disabled.
403source_not_allowedThe Application has a source-address allow-list in enforce mode and the call came from another address. Treat as a denial; the organization admin has been alerted and can approve a new address in the admin console.
404unknown_userNo such active user in your org, or the user has no access grant to this app (the two are deliberately indistinguishable).
409user_has_no_registered_deviceUser must enroll a device first.
429too_many_requestsPush throttle: after 3 denied or unanswered requests in a row (15 min window) the user is paused for 15 minutes; also at most 10 requests per 10 minutes. Honour the Retry-After header and treat it as a denial.

GET /auth/request/:id

Poll a request. Requires the same API key; you can only read your own Application's requests.

{ "request_id": "req_...", "status": "pending" }

status is one of pending, approved, denied, expired, cancelled, failed. When approved, two extra fields appear:

{
  "request_id": "req_...",
  "status": "approved",
  "result_token": "eyJhbGciOiJFZERTQS...",   // EdDSA JWT — verify it
  "jwks_url": "https://mfa.tenka.cloud/api/v1/.well-known/jwks.json"
}

When denied or failed, a reason field is included.

POST /auth/request/:id/cancel

Cancel a pending request (e.g. the user navigated away). Returns { "ok": true, "status": "cancelled" }.

GET /.well-known/jwks.json

Public keys for verifying result tokens. No API key required; cache up to 5 minutes.

{ "keys": [ { "kty": "OKP", "crv": "Ed25519", "x": "...", "kid": "...", "alg": "EdDSA" } ] }

Verifying the result token

The result_token is an EdDSA (Ed25519) JWT. Always verify it — treat it, not the bare status, as the proof of authentication. Check:

Claims:

ClaimMeaning
resultAlways "approved" (a token is only minted on approval).
subStable Tenka user id.
email / nameThe authenticated user.
org_id / org_nameThe user's organization.
app_nameYour Application's name.
amrMethods used, e.g. ["webauthn"].
auth_timeWhen the user approved (epoch seconds).
aud / iss / exp / iat / jtiStandard JWT claims (verify these).

Full example

1 — Start the request:

curl -s https://mfa.tenka.cloud/api/v1/auth/request \
  -H "Authorization: Bearer $TENKA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"username":"jon@example.com","reason":"VPN login","location":"Stockholm, SE"}'

2 — Poll until resolved:

curl -s https://mfa.tenka.cloud/api/v1/auth/request/req_abc123 \
  -H "Authorization: Bearer $TENKA_API_KEY"

3 — Verify the token (Node.js, using jose):

import { jwtVerify, createRemoteJWKSet } from 'jose';

const JWKS = createRemoteJWKSet(
  new URL('https://mfa.tenka.cloud/api/v1/.well-known/jwks.json'));

const { payload } = await jwtVerify(resultToken, JWKS, {
  issuer: 'https://mfa.tenka.cloud',
  audience: MY_APPLICATION_ID,   // the Application id from the admin console
});
// payload.sub, payload.email — the verified user. Now log them in.

Security notes

Questions or need an Application provisioned? Contact your tenka.auth administrator.