Auth

Login for first-party clients that can't use a pre-issued API key — username/password (+ MFA) or a passkey.

Login

POST /auth/login

Log in with username/password (+ MFA if enabled)

No key required

Request body

FieldTypeFlagsRequiredNotes
email string — Required The account's login email.
password string — Required The account's password, in plain text over TLS — never stored or logged.

Example request

curl -X POST "https://api.openstudio.one/v1/auth/login" \
  -H "Content-Type: application/json" \
  -d '{"email":"...","password":"..."}'
<?php
$ch = curl_init('https://api.openstudio.one/v1/auth/login');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
    'email' => '...',
    'password' => '...',
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);

$response = curl_exec($ch);
$data = json_decode($response, true);
import requests

headers = {
}

body = {
    'email': '...',
    'password': '...',
}
response = requests.post("https://api.openstudio.one/v1/auth/login", headers=headers, json=body)
data = response.json()
fetch('https://api.openstudio.one/v1/auth/login', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    'email': '...',
    'password': '...',
  })
})
  .then(res => res.json())
  .then(data => console.log(data));

Response fields

FieldTypeFlagsNotes
status string — success: login is complete, api_key is included. mfa_required: a further verification step is needed — see POST /auth/login/verify.
success
mfa_required
api_key string Only present when status is "success". The account's personal API key — its EXISTING key if it already has one (never rotated just because of this login), or a freshly minted one if it didn't have one yet. Use this as X-Api-User-Key on every other request.
profile object Only present when status is "success". Identical shape to GET /me's response.
mfa_type string Only present when status is "mfa_required".
otp_app
otp_mail
challenge_token string Only present when status is "mfa_required". Opaque, expires in 5 minutes — pass it to POST /auth/login/verify.

Example response

{
    "success": true,
    "data": {
        "status": "mfa_required",
        "mfa_type": "otp_mail",
        "challenge_token": "eyJ1aWQiOjk5OSwidHlwZSI6Im90cF9tYWlsIiwiZXhwIjoxNzcxNzAwMDAwfQ.4f2c9e7b1a8d6c5f3e2b1a0d9c8b7a6f"
    },
    "error": null
}

POST /auth/login/verify

Complete login with an MFA code

No key required

Request body

FieldTypeFlagsRequiredNotes
challenge_token string — Required From POST /auth/login's mfa_required response.
otp_code string — Required The 6-digit emailed code (otp_mail) or the current code from the authenticator app (otp_app).

Example request

curl -X POST "https://api.openstudio.one/v1/auth/login/verify" \
  -H "Content-Type: application/json" \
  -d '{"challenge_token":"...","otp_code":"..."}'
<?php
$ch = curl_init('https://api.openstudio.one/v1/auth/login/verify');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
    'challenge_token' => '...',
    'otp_code' => '...',
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);

$response = curl_exec($ch);
$data = json_decode($response, true);
import requests

headers = {
}

body = {
    'challenge_token': '...',
    'otp_code': '...',
}
response = requests.post("https://api.openstudio.one/v1/auth/login/verify", headers=headers, json=body)
data = response.json()
fetch('https://api.openstudio.one/v1/auth/login/verify', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    'challenge_token': '...',
    'otp_code': '...',
  })
})
  .then(res => res.json())
  .then(data => console.log(data));

Response fields

FieldTypeFlagsNotes
status string — Always success — a failed verification returns an error response instead.
success
api_key string — See POST /auth/login's own api_key field description.
profile object — Identical shape to GET /me's response.

Example response

{
    "success": true,
    "data": {
        "status": "success",
        "api_key": "a1b2c3d4e5f6...",
        "profile": {
            "users": {
                "user_id": 999,
                "username": "j.doe"
            }
        }
    },
    "error": null
}

POST /auth/websession

Bridge into a real web-dashboard session for an in-app WebView

User key

Request body

FieldTypeFlagsRequiredNotes
path string — Required A path on client.openstudio.one — "/", "/?view={key}" where {key} matches an app's key/view from GET /applications (letters/digits/underscore/hyphen only), OR one of a small fixed set of internal-but-not-view-routed paths (a handful of catalog apps that render url_type: "external" on web but are genuinely internal client.openstudio.one pages, just under a different top-level route — see web_session_bridge.php's WEB_SESSION_ALLOWED_INTERNAL_PATHS). Anything else is rejected with a 422 — this is an allow-list, not a sanitizer.

Example request

curl -X POST "https://api.openstudio.one/v1/auth/websession" \
  -H "X-Api-User-Key: YOUR_USER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"path":"..."}'
<?php
$ch = curl_init('https://api.openstudio.one/v1/auth/websession');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
    'path' => '...',
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY', 'Content-Type: application/json']);

$response = curl_exec($ch);
$data = json_decode($response, true);
import requests

headers = {
    "X-Api-User-Key": "YOUR_USER_KEY",
}

body = {
    'path': '...',
}
response = requests.post("https://api.openstudio.one/v1/auth/websession", headers=headers, json=body)
data = response.json()
fetch('https://api.openstudio.one/v1/auth/websession', {
  method: 'POST',
  headers: {
    'X-Api-User-Key': 'YOUR_USER_KEY',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    'path': '...',
  })
})
  .then(res => res.json())
  .then(data => console.log(data));

Response fields

FieldTypeFlagsNotes
redeem_url string — Load this URL in the WebView — not path itself.

Example response

{
    "success": true,
    "data": {
        "redeem_url": "https://www.openstudio.one/websession_redeem.php?token=3f9a2b1c..."
    },
    "error": null
}

Google

POST /auth/google

Sign in with Google

No key required

Request body

FieldTypeFlagsRequiredNotes
id_token string — Required The Google ID token (a signed JWT) from the app's own Google sign-in flow.

Example request

curl -X POST "https://api.openstudio.one/v1/auth/google" \
  -H "Content-Type: application/json" \
  -d '{"id_token":"..."}'
<?php
$ch = curl_init('https://api.openstudio.one/v1/auth/google');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
    'id_token' => '...',
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);

$response = curl_exec($ch);
$data = json_decode($response, true);
import requests

headers = {
}

body = {
    'id_token': '...',
}
response = requests.post("https://api.openstudio.one/v1/auth/google", headers=headers, json=body)
data = response.json()
fetch('https://api.openstudio.one/v1/auth/google', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    'id_token': '...',
  })
})
  .then(res => res.json())
  .then(data => console.log(data));

Response fields

FieldTypeFlagsNotes
status string — Always success — an unlinked Google account returns an error response instead.
success
api_key string — See POST /auth/login's own api_key field description.
profile object — Identical shape to GET /me's response.

Example response

{
    "success": true,
    "data": {
        "status": "success",
        "api_key": "a1b2c3d4e5f6...",
        "profile": {
            "users": {
                "user_id": 999,
                "username": "j.doe"
            }
        }
    },
    "error": null
}

Passkeys

POST /auth/passkey/register/start

Begin registering a passkey for the signed-in account

User key

Example request

curl -X POST "https://api.openstudio.one/v1/auth/passkey/register/start" \
  -H "X-Api-User-Key: YOUR_USER_KEY"
<?php
$ch = curl_init('https://api.openstudio.one/v1/auth/passkey/register/start');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY']);

$response = curl_exec($ch);
$data = json_decode($response, true);
import requests

headers = {
    "X-Api-User-Key": "YOUR_USER_KEY",
}

response = requests.post("https://api.openstudio.one/v1/auth/passkey/register/start", headers=headers)
data = response.json()
fetch('https://api.openstudio.one/v1/auth/passkey/register/start', {
  method: 'POST',
  headers: {
    'X-Api-User-Key': 'YOUR_USER_KEY'
  }
})
  .then(res => res.json())
  .then(data => console.log(data));

Response fields

FieldTypeFlagsNotes
create_args object — Standard WebAuthn PublicKeyCredentialCreationOptions, passed unmodified to the platform's passkey-creation API.
registration_token string — Opaque, expires in 5 minutes — pass it to POST /auth/passkey/register/finish along with the resulting credential.

Example response

{
    "success": true,
    "data": {
        "create_args": {
            "publicKey": {
                "rp": {
                    "id": "openstudio.one"
                }
            }
        },
        "registration_token": "eyJraW5kIjoicmVnaXN0ZXIiLCJ1aWQiOjk5OX0.4f2c9e7b1a8d6c5f"
    },
    "error": null
}

POST /auth/passkey/register/finish

Complete passkey registration

User key

Request body

FieldTypeFlagsRequiredNotes
registration_token string — Required From POST /auth/passkey/register/start.
device_name string — Optional User-supplied label, e.g. "iPhone 17".
clientDataJSON string — Required Base64url, from the native passkey-creation result.
attestationObject string — Required Base64url, from the native passkey-creation result.

Example request

curl -X POST "https://api.openstudio.one/v1/auth/passkey/register/finish" \
  -H "X-Api-User-Key: YOUR_USER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"registration_token":"...","device_name":"...","clientDataJSON":"...","attestationObject":"..."}'
<?php
$ch = curl_init('https://api.openstudio.one/v1/auth/passkey/register/finish');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
    'registration_token' => '...',
    'device_name' => '...',
    'clientDataJSON' => '...',
    'attestationObject' => '...',
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY', 'Content-Type: application/json']);

$response = curl_exec($ch);
$data = json_decode($response, true);
import requests

headers = {
    "X-Api-User-Key": "YOUR_USER_KEY",
}

body = {
    'registration_token': '...',
    'device_name': '...',
    'clientDataJSON': '...',
    'attestationObject': '...',
}
response = requests.post("https://api.openstudio.one/v1/auth/passkey/register/finish", headers=headers, json=body)
data = response.json()
fetch('https://api.openstudio.one/v1/auth/passkey/register/finish', {
  method: 'POST',
  headers: {
    'X-Api-User-Key': 'YOUR_USER_KEY',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    'registration_token': '...',
    'device_name': '...',
    'clientDataJSON': '...',
    'attestationObject': '...',
  })
})
  .then(res => res.json())
  .then(data => console.log(data));

Response fields

FieldTypeFlagsNotes
status string — Always success — a failed registration returns an error response instead.
success
id integer — The new passkey's id — usable with DELETE /auth/passkey.

Example response

{
    "success": true,
    "data": {
        "status": "success",
        "id": 8
    },
    "error": null
}

POST /auth/passkey/login/start

Begin signing in with a passkey

No key required

Example request

curl -X POST "https://api.openstudio.one/v1/auth/passkey/login/start"
<?php
$ch = curl_init('https://api.openstudio.one/v1/auth/passkey/login/start');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, []);

$response = curl_exec($ch);
$data = json_decode($response, true);
import requests

headers = {
}

response = requests.post("https://api.openstudio.one/v1/auth/passkey/login/start", headers=headers)
data = response.json()
fetch('https://api.openstudio.one/v1/auth/passkey/login/start', {
  method: 'POST',
  headers: {
    
  }
})
  .then(res => res.json())
  .then(data => console.log(data));

Response fields

FieldTypeFlagsNotes
get_args object — Standard WebAuthn PublicKeyCredentialRequestOptions, passed unmodified to the platform's passkey-assertion API.
login_token string — Opaque, expires in 5 minutes — pass it to POST /auth/passkey/login/finish along with the resulting assertion.

Example response

{
    "success": true,
    "data": {
        "get_args": {
            "publicKey": {
                "rpId": "openstudio.one"
            }
        },
        "login_token": "eyJraW5kIjoibG9naW4ifQ.9d1f3e2b1a0d9c8b"
    },
    "error": null
}

POST /auth/passkey/login/finish

Complete passkey login

No key required

Request body

FieldTypeFlagsRequiredNotes
login_token string — Required From POST /auth/passkey/login/start.
clientDataJSON string — Required Base64url, from the native passkey-assertion result.
authenticatorData string — Required Base64url, from the native passkey-assertion result.
signature string — Required Base64url, from the native passkey-assertion result.
rawId string — Required Base64url credential id, from the native passkey-assertion result — identifies which registered passkey was used.

Example request

curl -X POST "https://api.openstudio.one/v1/auth/passkey/login/finish" \
  -H "Content-Type: application/json" \
  -d '{"login_token":"...","clientDataJSON":"...","authenticatorData":"...","signature":"...","rawId":"..."}'
<?php
$ch = curl_init('https://api.openstudio.one/v1/auth/passkey/login/finish');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
    'login_token' => '...',
    'clientDataJSON' => '...',
    'authenticatorData' => '...',
    'signature' => '...',
    'rawId' => '...',
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);

$response = curl_exec($ch);
$data = json_decode($response, true);
import requests

headers = {
}

body = {
    'login_token': '...',
    'clientDataJSON': '...',
    'authenticatorData': '...',
    'signature': '...',
    'rawId': '...',
}
response = requests.post("https://api.openstudio.one/v1/auth/passkey/login/finish", headers=headers, json=body)
data = response.json()
fetch('https://api.openstudio.one/v1/auth/passkey/login/finish', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    'login_token': '...',
    'clientDataJSON': '...',
    'authenticatorData': '...',
    'signature': '...',
    'rawId': '...',
  })
})
  .then(res => res.json())
  .then(data => console.log(data));

Response fields

FieldTypeFlagsNotes
status string — Always success — a failed sign-in returns an error response instead.
success
api_key string — See POST /auth/login's own api_key field description.
profile object — Identical shape to GET /me's response.

Example response

{
    "success": true,
    "data": {
        "status": "success",
        "api_key": "a1b2c3d4e5f6...",
        "profile": {
            "users": {
                "user_id": 999,
                "username": "j.doe"
            }
        }
    },
    "error": null
}

GET /auth/passkey

List your registered passkeys

User key

Example request

curl -X GET "https://api.openstudio.one/v1/auth/passkey" \
  -H "X-Api-User-Key: YOUR_USER_KEY"
<?php
$ch = curl_init('https://api.openstudio.one/v1/auth/passkey');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY']);

$response = curl_exec($ch);
$data = json_decode($response, true);
import requests

headers = {
    "X-Api-User-Key": "YOUR_USER_KEY",
}

response = requests.get("https://api.openstudio.one/v1/auth/passkey", headers=headers)
data = response.json()
fetch('https://api.openstudio.one/v1/auth/passkey', {
  method: 'GET',
  headers: {
    'X-Api-User-Key': 'YOUR_USER_KEY'
  }
})
  .then(res => res.json())
  .then(data => console.log(data));

ⓘRuns a real request from your browser directly to auth/passkey. Keys are only held in this page's memory while you're here — never stored, never sent anywhere else.

Response fields

FieldTypeFlagsNotes
passkeys array of objects — Every passkey registered on this account.
↳id integer — Usable with DELETE /auth/passkey.
↳device_name string User-supplied label, if one was given at registration.
↳user_agent string Raw value recorded at registration time — a browser user-agent string, or this app's own, depending on where the passkey was created.
↳last_used string UTC timestamp (YYYY-MM-DD HH:MM:SS) of the last successful login with this passkey. null if never used to log in since registration.
↳created_at string — UTC timestamp (YYYY-MM-DD HH:MM:SS) of registration.

Example response

{
    "success": true,
    "data": {
        "passkeys": [
            {
                "id": 8,
                "device_name": "iPhone 17",
                "user_agent": "OpenStudio-Mobile/1.0",
                "last_used": "2026-08-23 09:12:00",
                "created_at": "2026-08-19 10:41:35"
            }
        ]
    },
    "error": null
}

DELETE /auth/passkey

Revoke a passkey

User key

Request body

FieldTypeFlagsRequiredNotes
passkey_id integer — Required The passkey to remove, from GET /auth/passkey's own list.

Example request

curl -X DELETE "https://api.openstudio.one/v1/auth/passkey" \
  -H "X-Api-User-Key: YOUR_USER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"passkey_id":123}'
<?php
$ch = curl_init('https://api.openstudio.one/v1/auth/passkey');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
    'passkey_id' => 123,
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY', 'Content-Type: application/json']);

$response = curl_exec($ch);
$data = json_decode($response, true);
import requests

headers = {
    "X-Api-User-Key": "YOUR_USER_KEY",
}

body = {
    'passkey_id': 123,
}
response = requests.delete("https://api.openstudio.one/v1/auth/passkey", headers=headers, json=body)
data = response.json()
fetch('https://api.openstudio.one/v1/auth/passkey', {
  method: 'DELETE',
  headers: {
    'X-Api-User-Key': 'YOUR_USER_KEY',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    'passkey_id': 123,
  })
})
  .then(res => res.json())
  .then(data => console.log(data));

Response fields

FieldTypeFlagsNotes

Example response

{
    "success": true,
    "data": {},
    "error": null
}
ESC