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

TermMeaning
HabitA saved question with a type (noul, choice or score), wording and answers, named username.collection.habit.
@latestThe 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.
CollectionThe middle part of the name (acme.email). Related habits that you list and share together.

Ask habits

POST/v1/systemone

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?" }
  }
}
FieldTypeDescription
staterequiredstring or objectThe input to decide about. Objects are sent as JSON.
questionsrequiredobjectYour question ids mapped to habit or inline questions.
storebooleanKeep 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" }
}
FieldDescription
noul · choice · scoreThe typed answer, in the same shapes as any System One question, with probabilities and confidence for choice and score.
decisionThe habit's yes or no for noul answers it learned. Prefer it to rounding noul.
habitThe version that answered.
answered_bylearned or model. The price is the same either way.
cost_usdWhat this answer cost.
usage.full_price_usdWhat the request would cost at the first-100 rate, for comparison.
request_idA signed id for corrections. Present when the request asked at least one habit.

Batch

POST/v1/systemone/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

POST/v1/habits
FieldDescription
namerequiredusername.collection.habit: lowercase letters, digits and hyphens, under your own username.
typerequirednoul, choice or score.
instructionsrequiredThe question.
criterianoul: {"true": "…", "false": "…"} (optional). choice: {"option": "description", …}. score: ["lowest level", …, "highest level"].

New habits are private. They answer from the first request.

Edit @latest

PATCH/v1/habits/{name}
{ "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

POST/v1/habits/{name}/publish

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

GET/v1/habits/{name}
FieldDescription
statuslearning or learned. Owner only.
accuracyAccuracy on your corrections it hasn't trained on. Owner only.
labelsCorrections and examples received. Owner only.
last_30dDecisions, what they cost, their price at the first-100 rate, and what you saved. Owner only.
latestThe current wording and answers of @latest. Owner only.
publishedEvery published version with its wording and answers.
priceThe habit's two rates.

List and search

GET/v1/habits?owner=acme
GET/v1/habits/acme.email
GET/v1/habits?q=email

List a user's habits, one collection, or search public habits.

Fork

POST/v1/habits
{ "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

POST/v1/labels
{ "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

POST/v1/habits/{name}/labels
{ "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.

StatusCodeWhen
400invalid_request · invalid_jsonA required field is missing or the body isn't JSON.
400invalid_habit_nameThe name isn't username.collection.habit.
400habit_question_overridesA habit question carried more than type and habit.
400habit_version_requiredSomeone else's habit was asked without a published version number.
400not_enough_decisionsPublishing before the habit's first 100 decisions.
400invalid_type · invalid_definitionThe habit definition is incomplete or unsupported.
400invalid_answerA correction doesn't match the habit's answers.
400immutable_fieldTried to change something that can't be changed.
400invalid_request_id · state_mismatch · state_required · question_not_a_habitA correction can't be matched to the request it names.
400batch_too_large · too_many_casesOver 256 batch items or 2,000 examples in one call.
401unauthenticatedMissing or unknown API key.
403habit_forbiddenThe habit is private, or it isn't yours to change.
404habit_not_found · habit_version_not_found · request_not_foundNothing exists at that name, version or request.
409habit_exists · habit_definition_conflictThe name is taken, or an edit changes nothing.
422habit_type_mismatchThe question's type differs from the habit's.
429spend_cap_reachedToday's spend reached your account's daily cap.

Limits

LimitValue
Batch items256 per request
Examples per call2,000
Input a habit learns fromThe first 3,000 characters of state
PublishingAfter a habit's first 100 decisions
NamesLowercase 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 habitPrice
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.