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:
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]"}'{
"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:
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
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
| Where | Sign in with | Keep the session in |
|---|---|---|
| A web page on a Kodkila host | POST /auth/login, refresh with POST /auth/refresh | The httpOnly cookies the API sets. Never read the token in JavaScript. |
| A website on your own domain | POST /auth/login-app from a small same-origin route on your server | httpOnly, Secure cookies on your own domain |
| A phone app | POST /auth/login-app, refresh with POST /auth/refresh-app | The 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.