Habits API
Ask saved System One questions by name. They learn from your traffic and corrections, and cost half as much from their 101st decision.
Base URL https://habits.darmousseh.com. Requests and responses are JSON. Money is reported in US dollars as decimal strings.
Quickstart
1. Create a habit
curlcurl https://habits.darmousseh.com/v1/habits \
-H "Authorization: Bearer $HABITS_KEY" -H "Content-Type: application/json" \
-d '{
"name": "acme.email.archive",
"type": "noul",
"instructions": "Can I archive this email without reading it?",
"criteria": { "true": "Marketing, newsletters, routine notifications.",
"false": "A real person, a bill, anything to act on." }
}'
2. Ask it
curlcurl https://habits.darmousseh.com/v1/systemone \
-H "Authorization: Bearer $HABITS_KEY" -H "Content-Type: application/json" \
-d '{
"state": "From: Robinhood <noreply@robinhood.com>\nSubject: Discover what analysts are into",
"questions": { "archive": { "type": "noul", "habit": "acme.email.archive" } }
}'
3. Correct it when it's wrong
curlcurl https://habits.darmousseh.com/v1/labels \
-H "Authorization: Bearer $HABITS_KEY" -H "Content-Type: application/json" \
-d '{ "cases": [ { "request_id": "req_…", "question": "archive", "answer": false,
"state": "From: Robinhood <noreply@robinhood.com>\nSubject: Discover what analysts are into" } ] }'
That's it. The habit keeps learning on its own and from every correction you send.
Authentication
Send your API key as a bearer token on every request: Authorization: Bearer hb_…. Create and revoke keys in the console. No account yet? Request access.
Concepts
| Term | Meaning |
|---|---|
| Habit | A saved question with a type (noul, choice or score), wording and answers, named username.collection.habit. |
@latest | The version that keeps learning. A bare name means @latest. Only its owner can ask it. |
| Published version | @1, @2, … A frozen copy of @latest: same wording, same answers, never retrained. Anyone can ask a published version. |
| Collection | The middle part of the name (acme.email). Related habits that you list and share together. |
Ask habits
One input (state) and any number of questions. A question is either a habit question, which names a saved habit, or an inline question, which carries its own wording like any System One request.
request{
"state": "From: Lakeside Utilities Subject: Your invoice #4471 is past due",
"questions": {
"archive": { "type": "noul", "habit": "acme.email.archive" },
"reply": { "type": "noul", "habit": "habits.email.worth-replying@1" },
"urgent": { "type": "noul", "instructions": "Does this need a reply today?" }
}
}
| Field | Type | Description |
|---|---|---|
staterequired | string or object | The input to decide about. Objects are sent as JSON. |
questionsrequired | object | Your question ids mapped to habit or inline questions. |
store | boolean | Keep the input with the request, so a later correction can point at its request_id alone. |
A habit question takes only type and habit; adding wording or criteria is rejected with habit_question_overrides, so a request can never change what a habit means. Use a bare name or @latest for your own habits, and a published number (@1) for anyone else's.
Answers
response{
"request_id": "req_eyJ1Ijoi…",
"answers": {
"archive": { "type": "noul", "noul": 0.04, "decision": false, "habit": "acme.email.archive@latest",
"answered_by": "learned", "cost_usd": "0.0000062" },
"reply": { "type": "noul", "noul": 0.88, "habit": "habits.email.worth-replying@1",
"answered_by": "model", "cost_usd": "0.0000062" },
"urgent": { "type": "noul", "noul": 0.81, "answered_by": "model", "cost_usd": "0.0000124" }
},
"usage": { "input_tokens": 412, "cost_usd": "0.0000248", "full_price_usd": "0.0000372" }
}
| Field | Description |
|---|---|
noul · choice · score | The typed answer, in the same shapes as any System One question, with probabilities and confidence for choice and score. |
decision | The habit's yes or no for noul answers it learned. Prefer it to rounding noul. |
habit | The version that answered. |
answered_by | learned or model. The price is the same either way. |
cost_usd | What this answer cost. |
usage.full_price_usd | What the request would cost at the first-100 rate, for comparison. |
request_id | A signed id for corrections. Present when the request asked at least one habit. |
Batch
Up to 256 inputs with the same questions. Each item carries your own id and gets its own request_id. Batching is the fastest way to ask habits in volume.
request{
"items": [ { "id": "msg_1", "state": "…" }, { "id": "msg_2", "state": "…" } ],
"questions": { "archive": { "type": "noul", "habit": "acme.email.archive" } }
}
// → { "results": [ { "id": "msg_1", "request_id": "req_…", "answers": {…}, "usage": {…} }, … ], "usage": {…} }
Save a question as a habit
Add save_as to an inline question to create a habit from it on first use. Asking again with the same wording reuses it; different wording returns habit_definition_conflict instead of silently redefining it.
{ "urgent": { "type": "noul", "instructions": "Does this need a reply today?", "save_as": "acme.email.urgent" } }
Create a habit
| Field | Description |
|---|---|
namerequired | username.collection.habit: lowercase letters, digits and hyphens, under your own username. |
typerequired | noul, choice or score. |
instructionsrequired | The question. |
criteria | noul: {"true": "…", "false": "…"} (optional). choice: {"option": "description", …}. score: ["lowest level", …, "highest level"]. |
New habits are private. They answer from the first request.
Edit @latest
{ "instructions": "Can I archive this email without reading it? Receipts count as routine." }
Change the wording or the answers of @latest. The type stays the same, your corrections carry over, and it relearns within a minute or two. Published versions are not affected.
Publish a version
Freezes @latest as the next numbered version (@1, @2, …) and makes the habit public. A published version never changes and never retrains, so anyone pinned to it gets the same answers over time. A habit can be published once it has made its first 100 decisions; before that the call returns not_enough_decisions. @latest stays private and keeps learning.
Report card
| Field | Description |
|---|---|
status | learning or learned. Owner only. |
accuracy | Accuracy on your corrections it hasn't trained on. Owner only. |
labels | Corrections and examples received. Owner only. |
last_30d | Decisions, what they cost, their price at the first-100 rate, and what you saved. Owner only. |
latest | The current wording and answers of @latest. Owner only. |
published | Every published version with its wording and answers. |
price | The habit's two rates. |
List and search
List a user's habits, one collection, or search public habits.
Fork
{ "name": "you.email.archive", "fork_from": "acme.email.archive@3" }
Starts your own habit from a published version, including what it had learned. The fork answers well from day one and becomes yours as you correct it. Forks never copy the original owner's corrections.
Correct it when it's wrong
{ "cases": [ { "request_id": "req_…", "question": "archive", "answer": false, "state": "…" } ] }
Correct a past answer by its request_id and question id. Send the original state with it, or ask with "store": true so it isn't needed. A state that doesn't match the request returns state_mismatch. Answers are typed like the habit: true/false for noul, an option name for choice, a level index for score.
Teach with examples
{ "cases": [ { "state": "Subject: 40% off everything this weekend", "answer": true } ] }
Send examples you already have, up to 2,000 per call. Labelling the same input again replaces its earlier label.
Errors
Errors return {"error": {"code", "message"}} with an HTTP status.
| Status | Code | When |
|---|---|---|
| 400 | invalid_request · invalid_json | A required field is missing or the body isn't JSON. |
| 400 | invalid_habit_name | The name isn't username.collection.habit. |
| 400 | habit_question_overrides | A habit question carried more than type and habit. |
| 400 | habit_version_required | Someone else's habit was asked without a published version number. |
| 400 | not_enough_decisions | Publishing before the habit's first 100 decisions. |
| 400 | invalid_type · invalid_definition | The habit definition is incomplete or unsupported. |
| 400 | invalid_answer | A correction doesn't match the habit's answers. |
| 400 | immutable_field | Tried to change something that can't be changed. |
| 400 | invalid_request_id · state_mismatch · state_required · question_not_a_habit | A correction can't be matched to the request it names. |
| 400 | batch_too_large · too_many_cases | Over 256 batch items or 2,000 examples in one call. |
| 401 | unauthenticated | Missing or unknown API key. |
| 403 | habit_forbidden | The habit is private, or it isn't yours to change. |
| 404 | habit_not_found · habit_version_not_found · request_not_found | Nothing exists at that name, version or request. |
| 409 | habit_exists · habit_definition_conflict | The name is taken, or an edit changes nothing. |
| 422 | habit_type_mismatch | The question's type differs from the habit's. |
| 429 | spend_cap_reached | Today's spend reached your account's daily cap. |
Limits
| Limit | Value |
|---|---|
| Batch items | 256 per request |
| Examples per call | 2,000 |
| Input a habit learns from | The first 3,000 characters of state |
| Publishing | After a habit's first 100 decisions |
| Names | Lowercase letters, digits and hyphens; three parts |
| Daily spend cap | $2 by default, adjustable from $0.50 to $100 in the console; requests return 429 spend_cap_reached until 00:00 UTC |
Pricing
| Decisions on a habit | Price |
|---|---|
| 1 to 100 | $0.04 per million input tokens |
| 101 and on | $0.02 per million input tokens, however it's answered |
| Inline questions | $0.04 per million input tokens |
Input tokens are your state plus the question. Output is free. Decisions are counted per habit, per account. Every answer reports its own cost_usd.