Preview. This site is still being built. Features, prices and terms may change, and nothing here is a binding offer. Read the preview notice

Preview. Still being built; nothing here is a binding offer. Details

Sign-in

Your users sign in to your channel with a code sent by email, or with a passkey. Kodkila keeps the accounts, so your app holds no passwords.

Name your channel

Sign-in routes need to know which channel the user is signing in to. Send your channel’s id as x-ref, or a live app key as X-App-Key. A test app key can’t start a sign-in, because sending a code is a write.

Email codes

Ask for a code. The answer is the same for every address, so it never tells anyone whether an account exists:

Terminal
curl -X POST https://api.intersink.com/v1/auth/request-otp \
  -H "Content-Type: application/json" \
  -H "x-ref: YOUR_CHANNEL_ID" \
  -d '{"email":"[email protected]"}'
Response data
{
  "message": "If this address can sign in here, a code is on its way.",
  "delivery": "email",
  "retryAfterSeconds": 30,
  "expiresInSeconds": 900,
  "codeLength": 6
}

The code is valid for 15 minutes. Exchange it for tokens:

Terminal
curl -X POST https://api.intersink.com/v1/auth/login-app \
  -H "Content-Type: application/json" \
  -H "x-ref: YOUR_CHANNEL_ID" \
  -d '{"email":"[email protected]","otp":"123456"}'

The answer holds access_token, refresh_token and user. Send the access token as Authorization: Bearer on later calls, and get a new pair from POST /auth/refresh-app with the refresh token when it runs out.

With the SDK

sign-in.ts
import { createKodkilaClient } from "@kodkila/sdk";

const CHANNEL_ID = process.env.KODKILA_CHANNEL_ID!; // your channel's id
let accessToken: string | null = null;

const api = createKodkilaClient({
  baseUrl: "https://api.intersink.com/v1",
  headers: { "x-ref": CHANNEL_ID },
  getAccessToken: () => accessToken,
});

// 1. Send a 6-digit code to the address. It is valid for 15 minutes.
export async function sendCode(email: string) {
  return api.post("/api/auth/request-otp", { body: { email } });
}

// 2. Exchange the code for tokens, then load the user.
export async function signIn(email: string, otp: string) {
  const res = await api.post("/api/auth/login-app", { body: { email, otp } });
  if ("mfa_required" in res) {
    // Two-step is on for this account: finish with one of res.methods.
    return { pendingToken: res.pending_token, methods: res.methods };
  }
  accessToken = res.access_token; // keep res.refresh_token in secure storage
  return { user: await api.get("/api/auth/me") };
}

Web, apps and servers

WhereSign in withKeep the session in
A web page on a Kodkila hostPOST /auth/login, refresh with POST /auth/refreshThe httpOnly cookies the API sets. Never read the token in JavaScript.
A website on your own domainPOST /auth/login-app from a small same-origin route on your serverhttpOnly, Secure cookies on your own domain
A phone appPOST /auth/login-app, refresh with POST /auth/refresh-appThe phone’s secure storage (Keychain or Keystore)

Refresh once when a call answers 401 with UNAUTHENTICATED or TOKEN_INVALID, share one refresh between parallel calls, and send the user to sign-in if the retry fails too. A wrong code or passkey has its own error code and never means “refresh”.

Two-step sign-in

When an account has an authenticator app or emailed second codes turned on, the code exchange answers mfa_required: true with a pending_token and the methods that work here. Finish with POST /auth/mfa/totp, POST /auth/mfa/otp or POST /auth/mfa/passkey/verify. The sign-in never goes through with fewer factors.

Passkeys

A signed-in user adds a passkey with POST /auth/passkey/register/options and POST /auth/passkey/register/verify. Later they can sign in with the passkey alone, through POST /auth/passkey/login/options and POST /auth/passkey/login/verify. Leave out the email for a usernameless sign-in.

A passkey works only on the domain it was made for. Kodkila picks that domain from the page that asks: a site on a domain your channel has verified gets passkeys for that domain. GET /auth/passkeys lists a user’s passkeys and says which ones work on the current site.

Other methods

Channels can also turn on passwords and Google or Apple sign-in. POST /auth/methods tells you which methods an address can use. The authentication reference has every route.

Limits

Codes can be resent after 30 seconds, then 60, then 120, with at most 5 codes per address and channel in 15 minutes. Every 429 carries retryAfterSeconds and a Retry-After header, so tell the user when they can try again.