← AI Life Coach / API
Your token

Drive AI Life Coach from your own code

Base URL https://api.skillsafe.ai/v1/app-api. There is one lane, so there is no task field — you post a goal and what has actually happened, and you get a plan back. Pick a language once and every example on the page follows it.

Before you build on this: AI Life Coach is a goal and habit coach. It is not therapy, it does not diagnose, and it does not give medical advice. The browser app runs a client-side triage before it spends anything and refuses to plan when someone may not be safe, when another person in the account is dangerous or controlling, or when the goal is a medical matter rather than a habit. If you wrap this API in something of your own, carry that screen across — and handle route_to_help, which is the model doing the same thing from its side.

The envelope

Every response is wrapped. Success is {"ok":true,"data":{...}}. Failure is {"ok":false,"error":{...},"meta":{...}}, where error carries code, message, status and details, and meta carries request_id and timestamp. Quote the request_id if you ever need to ask about a specific call. Write the unwrapping once, in step 1, and never think about it again.

Errors

HTTPerror.codeWhat it means
400validation_errorThe request itself was malformed — bad JSON, a missing path segment, a header the API cannot read. Note what this is not: it is not a complaint about the fields inside your run body. The run body is never validated. See step 4.
401unauthorizedNo token, or a token that has expired or been revoked. Mint a guest token, or sign in at /tokens.html for a personal one. On a first-ever visit this is the correct response, not a fault.
402insufficient_creditsThe balance is below the hold the run needs. Call /estimate first and compare it against /me — a 402 after the user has pressed submit is a failure of the client, not of the user.
404not_foundWrong path, or a job id that does not belong to the calling subject. Every guest token is its own subject, so a job started under one token is invisible to the next.
429rate_limitedToo many calls. Back off and retry; do not retry in a tight loop, and do not retry a metered run without checking whether the first one actually landed.
500server_errorThe run started and did not complete. If a stream died mid-object, keep the bytes you have — a cut plan is often nearly whole and worth repairing rather than throwing away.

1. A tiny client

Every call below is the same shape: a POST or GET to https://api.skillsafe.ai/v1/app-api with an Authorization: Bearer header, returning the envelope from above. Write the unwrapping once and the rest of this page is one line per call.

2. Get a token

A guest token is minted with no sign-in and is enough for /me and /estimate. Building a plan is metered, so it needs a personal token — sign in at /tokens.html and copy it from there. Guest identities are per-token: mint a second guest token and the first one's jobs are no longer yours to poll.

3. Check the session — GET /me

Returns exactly three fields: subject_type, subject_id and credits. There is no name or email on it, so the signed-in test is subject_type === "user" and nothing else. A 401 here on a token you never minted is the correct answer, not a fault. Read credits before you offer the user a button that spends them.

4. Price it first — POST /estimate

Free, and it creates no job. It returns hold_credits (what is reserved, priced against the full output cap), min_credits and the model binding — gpt-terra, which resolves to gpt-5.6-terra. Compare min_credits against the credits from step 3 before you show anyone a submit button.

The body you post IS the input object. Post the object itself — never a bare string, and never an {"input": ...} wrapper, which returns ok:true while quietly hiding every field from the model. And note what the server does with it: nothing. /estimate and /run perform no validation on this body at all. A bare string, null, [] or 42 each come back ok:true with a correct model binding and a plausible hold. There is no 400 waiting to catch your typo. Validate on your side, here, before you send.

5. Run it — POST /run, then poll

Metered, so it needs a personal token. Send an Idempotency-Key derived from the body: a network blip must not bill twice, and a retry that carries the same key returns the first result rather than starting a second run. Same body as /estimate, and the same warning applies — nothing on the server checks it.

The plan comes back as data.output_text, which is a JSON string holding the output object. Parse it; do not show it to anyone raw. A long run may answer with a job instead of a result, in which case data.status is queued or running and you poll GET /jobs/{job_id} with the same token until it reads succeeded or failed. Parse the result only after you have checked for route_to_help — step 7.

6. Stream it — POST /run-stream

POST /run-stream is the same run, same body, same Idempotency-Key, delivered as server-sent events. Frames are separated by a blank line and carry a named event: an event: line, then a data: line, then the blank line. The event names are job, delta, done, pending and error. The name is always on the event: line — the data: payload of a delta frame carries text and nothing else, so read the frame name from event: and never from a field inside data.

Accumulate the text of every delta frame; that concatenation is the plan, as the same JSON string that /run would have returned in output_text. The done frame carries charged_credits — the real price, normally well below the hold — along with job_id, status and truncated. An error frame is terminal and carries code and message. A pending frame in place of done means the run is continuing out of band; poll the job id from step 5.

On an idempotent replay the server may answer with plain JSON instead of text/event-stream. Check the content-type before you start reading lines, and fall back to the envelope path if it is not an event stream.

7. Handle route_to_help — the shape that is not a plan

The app runs a safety triage in the browser before it ever calls the API, and it stops there. An API caller has no such client, so the model carries its own copy of the same boundary: when the input describes a crisis, someone else being dangerous or controlling, or a goal that is a medical matter rather than a habit, it returns this and nothing else.

{"route_to_help": true}

It carries no other fields. It is not an error, not a truncated plan and not something to retry — a retry produces the same answer and bills you again. Treat it as a terminal, successful outcome that means do not present a plan, and show real support resources instead. Every example below checks for it before reading any other field, because the failure mode worth avoiding is rendering an empty plan card next to somebody who has just told you something serious.