POST
/
v1
/
auth
/
passkey
/
login
Start a passkey login
curl --request POST \
  --url https://api.next.orenda.finance/v1/auth/passkey/login \
  --header 'x-program-id: <x-program-id>'
import requests

url = "https://api.next.orenda.finance/v1/auth/passkey/login"

headers = {"x-program-id": "<x-program-id>"}

response = requests.post(url, headers=headers)

print(response.text)
const options = {method: 'POST', headers: {'x-program-id': '<x-program-id>'}};

fetch('https://api.next.orenda.finance/v1/auth/passkey/login', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));
<?php

$curl = curl_init();

curl_setopt_array($curl, [
CURLOPT_URL => "https://api.next.orenda.finance/v1/auth/passkey/login",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => [
"x-program-id: <x-program-id>"
],
]);

$response = curl_exec($curl);
$err = curl_error($curl);

curl_close($curl);

if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}
package main

import (
"fmt"
"net/http"
"io"
)

func main() {

url := "https://api.next.orenda.finance/v1/auth/passkey/login"

req, _ := http.NewRequest("POST", url, nil)

req.Header.Add("x-program-id", "<x-program-id>")

res, _ := http.DefaultClient.Do(req)

defer res.Body.Close()
body, _ := io.ReadAll(res.Body)

fmt.Println(string(body))

}
HttpResponse<String> response = Unirest.post("https://api.next.orenda.finance/v1/auth/passkey/login")
.header("x-program-id", "<x-program-id>")
.asString();
require 'uri'
require 'net/http'

url = URI("https://api.next.orenda.finance/v1/auth/passkey/login")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Post.new(url)
request["x-program-id"] = '<x-program-id>'

response = http.request(request)
puts response.read_body
{
  "success": true,
  "data": {
    "authenticated": false,
    "challenge": "PASSKEY",
    "session_token": "3f1c2e8a-9b4d-4e6f-8a1b-2c3d4e5f6a7b",
    "fido2options": {
      "challenge": "q1n9bXJ0c2VjdXJlY2hhbGxlbmdl",
      "timeout": 60000,
      "rpId": "next.orenda.finance",
      "userVerification": "preferred",
      "allowCredentials": [
        {
          "type": "public-key",
          "id": "AaIWjR2k8Qm3sV7tZ1pYxC"
        }
      ]
    }
  }
}
{
"success": false,
"code": "RESOURCE_NOT_FOUND",
"message": "Program issuer not found"
}
{
"success": false,
"code": "VALIDATION_ERROR",
"message": "email is required"
}
{
"success": false,
"code": "INTERNAL_SERVER_ERROR",
"message": "Internal Server Error"
}
Step 1 of signing in with a passkey. Call passkey/login (public — no bearer token) to get a session_token and fido2options, then unlock the passkey on the device. You finish on Passkey sign-in (finish).

Before you begin

Passkeys need a browser or app webview that supports WebAuthn. Check for support before you show a passkey option:
const passkeysSupported =
  window.PublicKeyCredential !== undefined &&
  (await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable());
Our API sends and expects binary values as base64url strings, while the browser works with byte arrays. The web example below uses these two helpers to convert between them:
// bytes → base64url
const toB64url = (buf) =>
  btoa(String.fromCharCode(...new Uint8Array(buf)))
    .replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");

// base64url → bytes
const fromB64url = (s) =>
  Uint8Array.from(atob(s.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0));
A WebAuthn helper library such as @simplewebauthn/browser does these conversions for you. The examples here show the raw approach so you can see exactly what’s happening.

The whole flow

This one function runs all the way through both calls to tokens. On Kotlin, Swift, and Flutter the device step uses the platform’s credential API — see Unlocking the passkey on the device.
async function signInWithPasskey(programId) {
  const base = "https://api.next.orenda.finance";
  const headers = { "Content-Type": "application/json", "x-program-id": programId };

  // 1. Start
  const start = await fetch(`${base}/v1/auth/passkey/login`, {
    method: "POST", headers,
  }).then((r) => r.json());
  const { session_token, fido2options } = start.data;

  // 2. Device — unlock the passkey
  fido2options.challenge = fromB64url(fido2options.challenge);
  (fido2options.allowCredentials ?? []).forEach((c) => { c.id = fromB64url(c.id); });
  const cred = await navigator.credentials.get({ publicKey: fido2options });

  // 3. Finish
  const result = await fetch(`${base}/v1/auth/passkey/verify`, {
    method: "POST", headers,
    body: JSON.stringify({
      session_token,
      assertion: {
        credentialIdB64: toB64url(cred.rawId),
        userHandleB64: toB64url(cred.response.userHandle),
        clientDataJSON_B64: toB64url(cred.response.clientDataJSON),
        authenticatorDataB64: toB64url(cred.response.authenticatorData),
        signatureB64: toB64url(cred.response.signature),
      },
    }),
  }).then((r) => r.json());

  return result.data; // access_token, id_token, refresh_token, ...
}
suspend fun signInWithPasskey(programId: String): JSONObject {
  val base = "https://api.next.orenda.finance"
  val client = OkHttpClient()
  fun post(path: String, body: String): JSONObject {
    val req = Request.Builder().url("$base$path")
      .addHeader("x-program-id", programId)
      .post(body.toRequestBody("application/json".toMediaType())).build()
    return JSONObject(client.newCall(req).execute().body!!.string())
  }

  // 1. Start
  val data = post("/v1/auth/passkey/login", "{}").getJSONObject("data")
  val sessionToken = data.getString("session_token")
  val fido2options = data.getJSONObject("fido2options")

  // 2. Device — unlock the passkey with Android Credential Manager (see below)
  val cred = getPasskey(fido2options)

  // 3. Finish
  val assertion = JSONObject()
    .put("credentialIdB64", cred.credentialIdB64)
    .put("userHandleB64", cred.userHandleB64)
    .put("clientDataJSON_B64", cred.clientDataJsonB64)
    .put("authenticatorDataB64", cred.authenticatorDataB64)
    .put("signatureB64", cred.signatureB64)
  return post("/v1/auth/passkey/verify", JSONObject()
    .put("session_token", sessionToken).put("assertion", assertion).toString())
    .getJSONObject("data")
}
func signInWithPasskey(programId: String) async throws -> [String: Any] {
  let base = "https://api.next.orenda.finance"
  func post(_ path: String, _ body: [String: Any]) async throws -> [String: Any] {
    var req = URLRequest(url: URL(string: base + path)!)
    req.httpMethod = "POST"
    req.setValue("application/json", forHTTPHeaderField: "Content-Type")
    req.setValue(programId, forHTTPHeaderField: "x-program-id")
    req.httpBody = try JSONSerialization.data(withJSONObject: body)
    let (data, _) = try await URLSession.shared.data(for: req)
    return try JSONSerialization.jsonObject(with: data) as! [String: Any]
  }

  // 1. Start
  let start = try await post("/v1/auth/passkey/login", [:])["data"] as! [String: Any]
  let sessionToken = start["session_token"] as! String
  let fido2options = start["fido2options"] as! [String: Any]

  // 2. Device — unlock the passkey with AuthenticationServices (see below)
  let cred = try await getPasskey(fido2options)

  // 3. Finish
  let result = try await post("/v1/auth/passkey/verify", [
    "session_token": sessionToken,
    "assertion": [
      "credentialIdB64": cred.credentialIdB64,
      "userHandleB64": cred.userHandleB64,
      "clientDataJSON_B64": cred.clientDataJSONB64,
      "authenticatorDataB64": cred.authenticatorDataB64,
      "signatureB64": cred.signatureB64,
    ],
  ])
  return result["data"] as! [String: Any]
}
Future<Map<String, dynamic>> signInWithPasskey(String programId) async {
  const base = 'https://api.next.orenda.finance';
  final headers = {'Content-Type': 'application/json', 'x-program-id': programId};

  // 1. Start
  final start = jsonDecode((await http.post(
    Uri.parse('$base/v1/auth/passkey/login'), headers: headers,
  )).body)['data'];
  final sessionToken = start['session_token'];
  final fido2options = start['fido2options'];

  // 2. Device — unlock the passkey with a passkeys plugin (see below)
  final cred = await getPasskey(fido2options);

  // 3. Finish
  final result = jsonDecode((await http.post(
    Uri.parse('$base/v1/auth/passkey/verify'),
    headers: headers,
    body: jsonEncode({
      'session_token': sessionToken,
      'assertion': {
        'credentialIdB64': cred.credentialIdB64,
        'userHandleB64': cred.userHandleB64,
        'clientDataJSON_B64': cred.clientDataJsonB64,
        'authenticatorDataB64': cred.authenticatorDataB64,
        'signatureB64': cred.signatureB64,
      },
    }),
  )).body)['data'];

  return result; // access_token, id_token, refresh_token, ...
}

Unlocking the passkey on the device

The device step turns the fido2options into a signed assertion. On the web that’s navigator.credentials.get() (shown above). On native platforms, use the OS credential API:
PlatformUse
AndroidCredential ManagerGetCredentialRequest with GetPublicKeyCredentialOption
iOSAuthenticationServicesASAuthorizationPlatformPublicKeyCredentialProvider assertion request
FlutterA plugin such as passkeys
Encode the result into the assertion that Passkey sign-in (finish) sends to passkey/verify.

Headers

x-program-id
string
required

Identifies the program. Can also be sent as the programId query parameter.

Response

Passkey challenge

success
boolean
Example:

true

data
object