curl --request POST \
--url https://api.phone-harness.com/sessions \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"timeout_seconds": 900
}
'import requests
url = "https://api.phone-harness.com/sessions"
payload = { "timeout_seconds": 900 }
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({timeout_seconds: 900})
};
fetch('https://api.phone-harness.com/sessions', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));{
"id": "abc123def456",
"state": "provisioning",
"provider": "shlut",
"device": null,
"close_requested": null,
"cleanup_pending": false,
"cleanup_status": null,
"billing_mode": "metered",
"phone": null,
"watch_url": "https://viewer.example.com/opaque-read-only-link",
"screen": null,
"age_s": 0,
"ready_age_s": 0,
"idle_s": 0,
"expires_at": 1800000900,
"timeout_seconds": 900,
"request_key": "example-request-key-0001"
}{
"error": "<string>",
"code": "<string>",
"state": "<string>",
"unsupported": true,
"outcome": "unknown",
"id": "<string>",
"request_key": "<string>",
"cleanup_complete": true,
"balance_cents": 123,
"session": "<string>",
"session_limit": 123,
"active_session_count": 123,
"available_session_slots": 123
}{
"error": "<string>",
"code": "<string>",
"state": "<string>",
"unsupported": true,
"outcome": "unknown",
"id": "<string>",
"request_key": "<string>",
"cleanup_complete": true,
"balance_cents": 123,
"session": "<string>",
"session_limit": 123,
"active_session_count": 123,
"available_session_slots": 123
}{
"error": "<string>",
"code": "<string>",
"state": "<string>",
"unsupported": true,
"outcome": "unknown",
"id": "<string>",
"request_key": "<string>",
"cleanup_complete": true,
"balance_cents": 123,
"session": "<string>",
"session_limit": 123,
"active_session_count": 123,
"available_session_slots": 123
}{
"error": "<string>",
"code": "<string>",
"state": "<string>",
"unsupported": true,
"outcome": "unknown",
"id": "<string>",
"request_key": "<string>",
"cleanup_complete": true,
"balance_cents": 123,
"session": "<string>",
"session_limit": 123,
"active_session_count": 123,
"available_session_slots": 123
}{
"error": "<string>",
"code": "<string>",
"state": "<string>",
"unsupported": true,
"outcome": "unknown",
"id": "<string>",
"request_key": "<string>",
"cleanup_complete": true,
"balance_cents": 123,
"session": "<string>",
"session_limit": 123,
"active_session_count": 123,
"available_session_slots": 123
}{
"error": "<string>",
"code": "<string>",
"state": "<string>",
"unsupported": true,
"outcome": "unknown",
"id": "<string>",
"request_key": "<string>",
"cleanup_complete": true,
"balance_cents": 123,
"session": "<string>",
"session_limit": 123,
"active_session_count": 123,
"available_session_slots": 123
}{
"error": "<string>",
"code": "<string>",
"state": "<string>",
"unsupported": true,
"outcome": "unknown",
"id": "<string>",
"request_key": "<string>",
"cleanup_complete": true,
"balance_cents": 123,
"session": "<string>",
"session_limit": 123,
"active_session_count": 123,
"available_session_slots": 123
}Create a session
Allocate an Android phone asynchronously. HTTP 202 means admitted, not ready. Poll the returned id. The deadline includes provisioning. Use one stable Idempotency-Key per logical Start. Pass your account’s profile_id (from GET /me) to use your own phone, which is saved when the session ends and comes back the same way next time; omit it for a temporary phone that keeps nothing. Raw capabilities and runtime resource overrides are not supported. Omit provider to use the service default.
curl --request POST \
--url https://api.phone-harness.com/sessions \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"timeout_seconds": 900
}
'import requests
url = "https://api.phone-harness.com/sessions"
payload = { "timeout_seconds": 900 }
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({timeout_seconds: 900})
};
fetch('https://api.phone-harness.com/sessions', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));{
"id": "abc123def456",
"state": "provisioning",
"provider": "shlut",
"device": null,
"close_requested": null,
"cleanup_pending": false,
"cleanup_status": null,
"billing_mode": "metered",
"phone": null,
"watch_url": "https://viewer.example.com/opaque-read-only-link",
"screen": null,
"age_s": 0,
"ready_age_s": 0,
"idle_s": 0,
"expires_at": 1800000900,
"timeout_seconds": 900,
"request_key": "example-request-key-0001"
}{
"error": "<string>",
"code": "<string>",
"state": "<string>",
"unsupported": true,
"outcome": "unknown",
"id": "<string>",
"request_key": "<string>",
"cleanup_complete": true,
"balance_cents": 123,
"session": "<string>",
"session_limit": 123,
"active_session_count": 123,
"available_session_slots": 123
}{
"error": "<string>",
"code": "<string>",
"state": "<string>",
"unsupported": true,
"outcome": "unknown",
"id": "<string>",
"request_key": "<string>",
"cleanup_complete": true,
"balance_cents": 123,
"session": "<string>",
"session_limit": 123,
"active_session_count": 123,
"available_session_slots": 123
}{
"error": "<string>",
"code": "<string>",
"state": "<string>",
"unsupported": true,
"outcome": "unknown",
"id": "<string>",
"request_key": "<string>",
"cleanup_complete": true,
"balance_cents": 123,
"session": "<string>",
"session_limit": 123,
"active_session_count": 123,
"available_session_slots": 123
}{
"error": "<string>",
"code": "<string>",
"state": "<string>",
"unsupported": true,
"outcome": "unknown",
"id": "<string>",
"request_key": "<string>",
"cleanup_complete": true,
"balance_cents": 123,
"session": "<string>",
"session_limit": 123,
"active_session_count": 123,
"available_session_slots": 123
}{
"error": "<string>",
"code": "<string>",
"state": "<string>",
"unsupported": true,
"outcome": "unknown",
"id": "<string>",
"request_key": "<string>",
"cleanup_complete": true,
"balance_cents": 123,
"session": "<string>",
"session_limit": 123,
"active_session_count": 123,
"available_session_slots": 123
}{
"error": "<string>",
"code": "<string>",
"state": "<string>",
"unsupported": true,
"outcome": "unknown",
"id": "<string>",
"request_key": "<string>",
"cleanup_complete": true,
"balance_cents": 123,
"session": "<string>",
"session_limit": 123,
"active_session_count": 123,
"available_session_slots": 123
}{
"error": "<string>",
"code": "<string>",
"state": "<string>",
"unsupported": true,
"outcome": "unknown",
"id": "<string>",
"request_key": "<string>",
"cleanup_complete": true,
"balance_cents": 123,
"session": "<string>",
"session_limit": 123,
"active_session_count": 123,
"available_session_slots": 123
}Authorizations
Phone Harness account API key (pck_…). The dashboard may use a Clerk session JWT with the same header. Never put an account key in a URL.
Headers
Same key plus same parsed JSON body reuses the session. Optional but recommended.
^[A-Za-z0-9_-]{16,128}$Body
Name what to start together with kind; omit both for your account's default (GET /me → default). Today: android + emulator for everyone, ios + device for an account whose own iPhone we have set up.
android, ios Together with platform. emulator for the shared Android phones; device for your own phone.
emulator, device Your account's profile id, from GET /me. The session then uses your own phone: its apps, logins and screen are saved when the session ends and restored for the next session started with the same profile_id. Only one session at a time may hold it, including the save after a session ends (409 profile_running). Omit for a temporary phone.
Total session lifetime from admission, including provisioning. Omitting it uses 300 seconds. No unlimited mode; must fit a supported absolute timestamp.
x >= 1Response
Admitted or replayed session.
Opaque session ID.
provisioning, ready, error, closing Display label in plain words ("Android emulator", "iPhone"); not a device connection address.
Time closure was requested. Unix time in seconds.
complete, allocation_uncertain, pending, null metered, disabled Legacy saved-phone ID. Null for a new disposable session.
Sensitive read-only snapshot link. Not the interactive owner viewer.
Show child attributes
Show child attributes
Rounded seconds since creation.
Rounded seconds since readiness, stopped at closure; zero before readiness.
Rounded seconds since the last activity recorded by the API. Does not extend or replace a selected session deadline.
The profile id this session holds, or null for a temporary phone.
Only while state is provisioning: where the phone is. queued means every boot slot is busy and the phone starts as soon as one frees; booting means the phone is starting. eta_seconds is a typical remaining time from recent boots on this worker, not a promise.
Show child attributes
Show child attributes
What the phone is: android or ios.
android, ios How it runs: an emulator, or a real device (your own phone on a host).
emulator, device Public startup failure, if present.
Absolute session expiry, including provisioning. Unix time in seconds.
Selected finite duration from creation, in seconds.
x >= 1Optional startup provenance. Presence is not a latency measurement or a guarantee that prepared starts are available.
Show child attributes
Show child attributes
Returned in a creation response when an idempotency key was supplied.
Present once the session is ready and ADB is enabled.
Show child attributes
Show child attributes