The headless SDK lets you run the full OwnID authentication flow without any pre-built UI components.
You control the UI; the SDK handles discovery, passkeys, verifications, and session creation.
Headless integration is currently available only for custom Integrations.
Prefer to start from code? Skip to the full example.
Initialize the headless client
Begin by loading the OwnID WebSDK:
<script>
((o,w,n,i,d)=>{o[i]=o[i]||(async(...a)=>((o[i].q=o[i].q||[]).push(a),{error:null,data:null})),
(d=w.createElement("script")).src='https://cdn.ownid.com/sdk/'+n,d.async=1,w.head.appendChild(d)})
(window,document,'<appID>','ownid');
</script>
Once loaded, get the headless instance and bind it to the user’s identifier (loginId).
const { headless } = window.ownid;
const hOwnid = headless.withContext({
loginId: 'user@example.com',
// or a phone number:
// loginId: '+15551234567',
});
Notes:
window.ownid is available after loading the OwnID script.
withContext returns a new bound instance of the SDK to a specific context with a loginId.
Discover available operations
Ask the SDK what authentication options exist for this user in this environment.
const discover = await hOwnid.auth.discover();
if ('error' in discover) throw discover;
// discover.operations may include:
// - PasskeyAuth
// - EmailVerification
// - PhoneNumberVerification
You’ll use discover.operations to choose the flow order.
Passkey Authentication
If the device supports WebAuthn and this user has a passkey, try that first.
if (
discover.operations.some((c) => c.type === 'PasskeyAuth') &&
(await hOwnid.passkeys.auth.isAvailable())
) {
const passkeyAuthCtrl = await hOwnid.passkeys.auth.start();
if ('error' in passkeyAuthCtrl) throw passkeyAuthCtrl;
const passkeyAuthRes = await passkeyAuthCtrl.whenSettled;
if ('error' in passkeyAuthRes) {
console.log('passkey auth error:', passkeyAuthRes);
console.log('fallback, e.g. email/phone verification');
} else {
return login('PasskeyAuth', passkeyAuthRes);
}
}
What’s happening:
- Find out if we expect passkey authentication to be available for the user if it’s listed in
discover.operations.
isAvailable() to confirm browser support.
start() triggers the passkey challenge.
- When the challenge settles -
whenSettled resolves to success or error.
- On success, we’ll attempt to create a session via
login(...).
- On error, we can choose how to handle, e.g. fall back to email/phone.
Email Verification
Notice that in order for Email Verification to be available - your OwnID app needs to be configured with an SMTP provider for sending emails.
if (discover.operations.some((c) => c.type === 'EmailVerification')) {
const challenge = await hOwnid.verifications.email.start();
// TODO: we use our own UI to collect the One Time Passcode (OTP).
const otp = prompt(`Check your email for an OTP:`);
const res = await challenge.complete({ otp });
if ('error' in res) throw res;
else return login('EmailVerification', res);
}
Phone Verification
Notice that in order for Phone Number Verification to be available - your OwnID app needs to be configured with an SMS provider for sending SMS messages.
if (discover.operations.some((c) => c.type === 'PhoneNumberVerification')) {
const challenge = await hOwnid.verifications.phone.start();
// TODO: we use our own UI to collect the One Time Passcode (OTP).
const otp = prompt(`Check your phone for an OTP:`);
const res = await challenge.complete({ otp });
if ('error' in res) throw res;
else return login('PhoneNumberVerification', res);
}
Establish a Session
async function login(auth, { accessToken }) {
const session = await hOwnid.withContext({ accessToken }).auth.login();
if ('error' in session) throw session;
const { sessionPayload } = session;
// TODO: establish your session.
}
Notes:
Recommended: Post-login Passkey Enrollment
async function login(auth, { accessToken }) {
const session = await hOwnid.withContext({ accessToken }).auth.login();
if ('error' in session) throw session;
const { sessionPayload } = session;
// TODO: establish your session.
if (auth !== 'PasskeyAuth' && (await hOwnid.passkeys.enroll.isAvailable())) {
const passkeyEnrollCtrl = await hOwnid.withContext(session).passkeys.enroll.start();
if ('error' in passkeyEnrollCtrl) {
console.log('ignoring start enroll error:', passkeyEnrollCtrl);
return;
}
const passkeyEnrollRes = await passkeyEnrollCtrl.whenSettled;
if ('error' in passkeyEnrollRes) {
console.log('ignoring enroll error:', passkeyEnrollRes);
return;
}
}
}
Notes:
- As passkey enrollment should be optional - failures should not affect the user-experience.
Full example
const { headless } = window.ownid;
const hOwnid = headless.withContext({
loginId: 'user@example.com',
// or a phone number:
// loginId: '+15551234567',
});
async function authenticate() {
// Discover available operations
const discover = await hOwnid.auth.discover();
if ('error' in discover) throw discover;
// Try passkey authentication (if available)
if (
discover.operations.some((c) => c.type === 'PasskeyAuth') &&
(await hOwnid.passkeys.auth.isAvailable())
) {
const passkeyAuthCtrl = await hOwnid.passkeys.auth.start();
if ('error' in passkeyAuthCtrl) throw passkeyAuthCtrl;
const passkeyAuthRes = await passkeyAuthCtrl.whenSettled;
if ('error' in passkeyAuthRes) {
console.log('passkey auth error:', passkeyAuthRes);
console.log('fallback, e.g. email/phone verification');
} else {
return await login('PasskeyAuth', passkeyAuthRes);
}
}
// Email verification flow
if (discover.operations.some((c) => c.type === 'EmailVerification')) {
const challenge = await hOwnid.verifications.email.start();
let res;
while (true) {
// TODO: Your own UI to collect the One Time Passcode (OTP).
const otp = prompt(`Check your email for an OTP:`);
res = await challenge.complete({ otp });
if ('error' in res) {
switch (res.error.code) {
case "verification_code_wrong":
continue;
case "maximum_attempts_reached":
default:
break;
}
}
}
if ('error' in res) throw res;
else return await login('EmailVerification', res);
}
// Phone verification flow
if (discover.operations.some((c) => c.type === 'PhoneNumberVerification')) {
const challenge = await hOwnid.verifications.phone.start();
// TODO: we use our own UI to collect the One Time Passcode (OTP).
const otp = prompt(`Check your phone for an OTP:`);
const res = await challenge.complete({ otp });
if ('error' in res) throw res;
else return await login('PhoneNumberVerification', res);
}
}
async function login(auth, { accessToken }) {
const session = await hOwnid.withContext({ accessToken }).auth.login();
if ('error' in session) throw session;
const { sessionPayload } = session;
// TODO: establish your session with sessionPayload from /getSession endpoint
// Recommended: Enroll a passkey after a non-passkey login
if (auth !== 'PasskeyAuth' && (await hOwnid.passkeys.enroll.isAvailable())) {
const passkeyEnrollCtrl = await hOwnid.withContext(session).passkeys.enroll.start();
if ('error' in passkeyEnrollCtrl) {
console.log('ignoring start enroll error:', passkeyEnrollCtrl);
return;
}
const passkeyEnrollRes = await passkeyEnrollCtrl.whenSettled;
if ('error' in passkeyEnrollRes) {
console.log('ignoring enroll error:', passkeyEnrollRes);
return;
}
}
}