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
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 >= 1Optional provider selector. Omit it to use the service default. The current production service uses shlut.
shlut Response
Admitted or replayed session.
Opaque session ID.
provisioning, ready, error, closing Provider selected for this session.
Display label; 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
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