Documentation ▸

Getting started

Send the facts of a transaction and get one of four answers back.

Himmel is an internal control your software calls. You send the facts of a transaction — or of an action you are about to take — and Himmel checks them against the rules your company has confirmed and the law that applies. You get back one of four answers, with the paragraph or clause behind it, and a check_id that gives the same answer back years later.

Himmel answers; your software enforces. No endpoint blocks an action, and none takes one.

Before your first check

A check is answered under rules your company has confirmed, and asked by the agent about to act. Three steps in the console, a few minutes in all:

  1. Open your account. Sign up, and open your company's account on Home: you are its first admin. Its short name, which you choose there, names your account's own rule sets (Opening your account).
  2. Switch a playbook on. On Home, start from Himmel's template for an online shop's support - refunds, retention discounts and goodwill credits - with your own figures: Himmel proposes none. Its page asks who its rules name, and one button confirms its rules and switches it on. The example below takes the refund your assistant gives on its own as up to €30. Or upload a playbook of your own: Himmel drafts its rules from it, and you confirm each one (Policies). Nothing runs until you have confirmed it.
  3. Register your agent, and make its test key. Under Agents, register it by its id in your systems and its role, and make it a test key: every check the key sends records the agent (A key of its own for every agent). A test key asks in test, where an answer is what live would give and counts toward nothing live.

Your first check

Every call carries an API key: here HIMMEL_KEY, your agent's test key. A check names a rule set, describes the action, and sends its facts. Here your support assistant asks before it refunds a customer €25, which the playbook you switched on lets it give on its own - policy.<your short name>.customer_support:

SHORT_NAME=example_shop   # your account's short name
TODAY=$(date -u +%F)
curl https://api.gethimmel.com/v1/check \
  -H "Authorization: Bearer $HIMMEL_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: case-4471-refund" \
  -d @- <<EOF
{
  "rule_set": "policy.$SHORT_NAME.customer_support",
  "transaction": {
    "ref": "CASE-4471",
    "status": "proposed"
  },
  "facts": {
    "action_date": "$TODAY",
    "action": "refund",
    "customer": { "name": "Kim Example", "customer_number": "C-1002" },
    "amount": { "amount": 2500, "currency": "EUR" },
    "order_total": { "amount": 6000, "currency": "EUR" },
    "delivered_on": "$TODAY",
    "reported_damaged": false
  }
}
EOF

An action asked about before it is taken is dated the day it is asked, today - a day either side for time zones - so the refund above is dated today.

The answer is determined, the playbook's layer says allowed, and it carries a check_id. "status": "proposed" means the refund has not happened yet, so it counts in no total. Your software gives the refund, then reports it:

curl https://api.gethimmel.com/v1/checks/$CHECK_ID/execution \
  -H "Authorization: Bearer $HIMMEL_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: case-4471-refund-done" \
  -d '{ "executed": true }'

From then on it counts, as a refund given does; and if anything its answer turns on changed while it was open, the report checks it again, and superseded_by names the answer that replaced yours. Had your software not given it, { "executed": false, "reason": "refused_by_caller" } says so, and it never counts. Ask about a €35 refund instead and the answer is hand_to_a_person: above €30 the playbook hands a refund to a person, whatever approval the case already holds. A key made for the agent says who acts on its own, so its requests need not name actor.

Money is always an integer number of minor units with its currency: €25.00 is 2500 EUR. Facts are things that can be observed — a date, an amount, which customer, who approved it — and never a conclusion such as "this refund is reasonable". An agent sends what it is about to do and where the instruction came from, never its own judgement of it. GET /v1/rules/{rule_set}/{version} lists the facts a rule set takes, your own policies' and our law's, which the catalogue shows too.

The four answers

Every check returns exactly one of four states, always with HTTP 200:

StateWhat it meansWhat your software does next
determinedA confirmed rule covers this, and every fact it needs was there.Act on the outcome, and keep the check_id with what you did.
undetermined_missing_factsA rule covers this, but needs facts that were not sent. missing names them.Fetch the named facts and check again.
not_coveredNo confirmed rule covers this: your policy does not say, or a rule is waiting for your rule owner.Do not act on it. Send it to a person.
contestedA rule covers this, but turns on a word it cannot apply mechanically, such as "exceptional".Do not act on it. The person named in signer decides.

A missing fact, an open question about who a recipient is, and a rule nobody has confirmed yet are all answers. None of them is an error.

Nothing runs until your rule owner confirms it

Until your rule owner has read and confirmed the rules, every check answers not_covered with the reason awaiting_confirmation. That is the product working as it should: a rule runs for your account only once a named person at your company has confirmed its wording and its worked examples. See Confirmations.

The check_id is the receipt

Store the check_id against whatever you then did. An answer is stored before it is sent, so the check is there the moment you read it. GET /v1/checks/{id} returns the answer exactly as it was given, for the whole retention period: a setting of your account, stated when it is opened, as the period your records must be kept for. An answer is never edited: when a later transaction changes an earlier one's treatment, Himmel issues a new answer for the earlier transaction and tells you in Alerts.

Before, or after

The same endpoint checks both directions:

  • A booking that happened is status: "executed", the default for your account's own keys and for someone signed in. It counts in every total from the moment it is stored. A key made for an agent says which it sends, proposed or executed: from it, status left out is refused (422 invalid-request, required), so nothing an agent sends is recorded as done because a field was left out.
  • An action about to be taken is status: "proposed". It is checked the same way, but it moves nobody's total until you report that it happened. This is how an agent asks before it acts. See Check. An agent's own tools ask this way through the SDK, in Python or JavaScript, the check in the code of the tool that acts; an agent whose runtime speaks MCP - the Claude Agent SDK, say - asks the same way through Himmel's MCP server, added by configuration with its own key: see Connect an agent by MCP.

Retrying a request

Send an Idempotency-Key header with any POST that changes something — a check, an execution report, a sign-off, a confirmation, an alert acknowledged, a match answered, a split, or a policy posted, drafted or published — and a retry with the same key and the same request within 24 hours gets the first answer again. Nothing is done twice, and a retry is never told that its own first attempt already happened. Because an answer is stored before it is sent, a connection that drops before you read it loses nothing: retry with the same key. The same key with a different request is refused with idempotency-conflict. A key belongs to one path and one environment: the same key sent to two alerts, or with a test key and a live key, is two requests. A request that is refused stores no key, so you can correct it and send it again with the same one. A policy's dry run stores nothing, so a retry is always safe and it takes no key.

Try the examples

Every rule set's own test cases are available as ready-to-send requests from GET /v1/sandbox/examples/{rule_set}. An example is a sequence: send its requests in order, because an answer can depend on what came before it — a customer's third goodwill credit in 90 days is answered in the light of the first two. Each request says who acts where the case does, and a case in which somebody asks before acting sends its last request as a proposal: what then becomes of it is yours to report, as your software would. Each call gives the recipients in its examples, and whoever acts in them, fresh ids, so running the examples twice does not add them up - except who acts where your key is given your account's own agent or person, as below. expect is the answer each case expects under the settings in needs_tenant_facts, and assumes says what else it takes for granted that no request can carry: rules it takes as not yet confirmed or as flagged, recipients it takes as one person, linked by your rule owner, when its transactions happened, or nobody acting where it names nobody. The sandbox account has the settings most examples assume; a few are about a company set up differently on purpose, and your own account's settings and confirmations may differ too — an answer that differs for one of those reasons is the rules working, not a fault. For a rule set whose countries are named, one example in its list is ours rather than the rule set's (source: api): the same booking in a country the rule set does not cover, which is not_covered in any account. A rule set's examples are there for an account that has it on, and are served in its terms: a person an example names through a setting - the team lead who approved - is the person your account names there; an action taken on a day no version of the rule set answers is moved, with every date of the example, into the version served; and to a key made for an agent, the agent acting is that agent - to one of your account's own keys, your account's one agent switched on, as the sandbox's own key is given its assistant. To one of your account's own keys, a person acting is one of your account's people, named as they are: a key of yours names only a person your account has as who acts - the sandbox has its team member, whom most of its playbook's examples name - so an example naming a person your account does not have says so in people, and your key is refused it. What cannot be put in your account's terms is said in assumes - people, the people it names that your account has nobody apart for; before_the_version, where an action taken could not be moved into a version that starts later; and agents, the agents it names that your account's own key cannot be given as one of yours, where your account has several switched on or none, so send it with the key of the agent it is about. An example given as one of your agents adds to that agent's own totals, where a total of its rule set counts each agent apart, and says so in agent_totals. Signed in to the console, Try it sends them for you, as you and in test, and sets each answer beside the one the example expects — saying whether it waits for your rule owner, or which of your settings, or which of the example's own assumptions, explains any difference. After an agent's example, it lets you report the action done or not done and ask again, as the agent's own software would.

Endpoints

GET/v1/sandbox/examples/{rule_set}Scope rules:read

The rule set's own test cases, as requests you can send

Each example is a sequence: send its requests in order, because an answer can depend on what was sent before it - a customer's third goodwill credit in 90 days is answered in the light of the first two. Each request says who acts where the case does, and one the case asks about before it is taken is a proposal: what then becomes of it is yours to report. A proposal is dated the day it is asked, so an example asked about is served dated today, every date in it moved with it - a refund five days after delivery stays five days after. Recipient ids, and the ids of who acts, carry recipient_suffix, new on every call, so examples sent twice do not add up. expect is the answer the case expects under needs_tenant_facts, and assumes says what else it takes for granted that no request carries - rules not yet confirmed or flagged, recipients linked, when things happened, nobody acting - so where your account differs, its answer does too; rules answer only once your rule owner has confirmed them. Each is served in your account's terms: a person it names through a setting is the person your account names there, an action taken dated before the version starts is moved into it with every date of the example, a key made for an agent is served its agent acting, and one of your account's own keys the one agent your account has switched on - what cannot be is said in assumes (people, before_the_version, agents), and an example served as your agent, where a total counts each agent's actions apart, says it adds to that agent's totals (agent_totals). source: api is one of ours rather than the rule set's: a booking in a country it does not cover, for a rule set that names its countries. A rule set is served to an account that has it on.

Needs a key with the scope rules:read. From a key made for an agent it reads that agent's own alone: another agent's action is 403 not-the-agents-action, another agent's record 403 insufficient-scope.

Parameters

  • rule_setpath · stringrequired
  • versionquery · string or null
  • statequery · string or null

    Only examples that expect this state

Responses

200 Successful response
  • rule_setstringrequired
  • versionstringrequired
  • recipient_suffixstringrequired

    Appended to every recipient id in this response, and to every id of who acts, and new on every call, so examples sent twice do not add up into one recipient's yearly total, or one actor's day.

  • exampleslist of SandboxExamplerequired
    8 fields
    • idstringrequired
    • source"case" | "api"required

      case: one of the rule set's own test cases. api: one of ours - a booking in a country the rule set does not cover - which no rule of the set answers.

    • examplestringrequired
    • expectobjectrequired

      The answer the case expects.

    • needs_tenant_factsobjectrequired

      The settings that answer assumes. Your own may differ.

    • assumesExampleAssumesrequired

      What else that answer takes for granted: empty where it is nothing.

      12 fields
      • unconfirmedlist of string or null

        Rules it takes as not yet confirmed by your rule owner.

      • flaggedlist of string or null

        Rules it takes as flagged.

      • linkedlist of list of string or null

        Its recipients it takes as one person, linked by your rule owner: each pair a record and the identity it counts as.

      • triggerobject or null

        The change it expects answers to be issued again from - reason, and for a link or split its recipients - where no request of its own makes that change.

      • as_ofstring or null

        The moment it is answered as of.

      • atmap of string or null

        When it takes its transactions to have happened, by the case's own id for each - case for the one it asks about: a request sent now happened now.

      • nobody_actsboolean or null

        It takes it that nobody is recorded as acting where it names nobody, and its rules read who acts. Sent signed in to the console, you are recorded instead.

      • decisionslist of object or null

        The questions it takes as decided on its action - question, answer, by - which no request carries: the person the question names signs it off on the answer that asked it (POST /v1/checks/{id}/resolution), and the action is answered again.

      • peoplelist of string or null

        The people it names - by the case's own ids - that your account has nobody apart for: a setting it names a person through that your account names nobody in, a person in no setting who is nobody of your account's, or two of its people your account names one person for. Every other person it names is served as the person your account names.

      • before_the_versionstring or null

        The first day of the version served, where its own action is taken before it and could not be moved into it - the version starting after today, say - so no version of it answers the action.

      • agentslist of string or null

        The agents it names acting - by the case's own ids - that it could not be served as an agent of yours: sent with one of your account's own keys, which names only an agent your account has registered, where your account has several switched on or none. Its requests name the case's agent with the fetch's suffix, which that key is refused for (unknown_agent): send it with the key of the agent it is about.

      • agent_totalsstring or null

        The agent of yours it is served as, where a total of its rule set counts each agent's actions apart: what it sends adds to that agent's own totals in the environment it is sent in, beside whatever the agent did there, so its answer may differ from what it expects.

    • sendlist of ExampleSteprequired
      3 fields
      • method"POST"required
      • pathstringrequired
      • bodyobjectrequired
    • rulestring or null
  • 401No API key was sent, or it is not in use (unauthorized).
  • 403The caller may not do this, and type says why: the key or role lacks the scope, the rule set is not enabled for this account, the person signed in belongs to no account, names were asked for by a caller that is not shown them, or a request acting in test tried to change the account's settings (live-only).
  • 404Nothing with that id or name exists for this account.
  • 422The request, or a fact in it, is not in the documented shape - a query parameter the operation does not take included, which is named (unknown_parameter) rather than ignored - or its Himmel-Env does not fit the caller. Missing facts are not errors.
  • 429More requests in a minute than the key's or the person's limit (rate-limited) - or, opening an account, more accounts in a day than the deployment opens, with Retry-After saying when to try again.
  • 500Something failed on our side. X-Request-Id identifies it in our logs.