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 requiredRequest body
| Field | Type | Flags | Required | Notes |
|---|---|---|---|---|
| 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
| Field | Type | Flags | Notes |
|---|---|---|---|
| status | string | — | success: login is complete, api_key is included. mfa_required: a further verification step is needed — see POST /auth/login/verify.successmfa_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_appotp_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 requiredRequest body
| Field | Type | Flags | Required | Notes |
|---|---|---|---|---|
| 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
| Field | Type | Flags | Notes |
|---|---|---|---|
| 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 keyRequest body
| Field | Type | Flags | Required | Notes |
|---|---|---|---|---|
| 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
| Field | Type | Flags | Notes |
|---|---|---|---|
| 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
}POST /auth/google
Sign in with Google
No key requiredRequest body
| Field | Type | Flags | Required | Notes |
|---|---|---|---|---|
| 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
| Field | Type | Flags | Notes |
|---|---|---|---|
| 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 keyExample 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
| Field | Type | Flags | Notes |
|---|---|---|---|
| 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 keyRequest body
| Field | Type | Flags | Required | Notes |
|---|---|---|---|---|
| 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
| Field | Type | Flags | Notes |
|---|---|---|---|
| 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 requiredExample 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
| Field | Type | Flags | Notes |
|---|---|---|---|
| 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 requiredRequest body
| Field | Type | Flags | Required | Notes |
|---|---|---|---|---|
| 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
| Field | Type | Flags | Notes |
|---|---|---|---|
| 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 keyExample 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
| Field | Type | Flags | Notes |
|---|---|---|---|
| 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 keyRequest body
| Field | Type | Flags | Required | Notes |
|---|---|---|---|---|
| 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
| Field | Type | Flags | Notes |
|---|
Example response
{
"success": true,
"data": {},
"error": null
}