---
title: "Quickstart"
description: "Mint a key, list the catalog, and make a real call — three HTTP requests against production, no account needed."
type: tutorial
audience: both
verified: 2026-09-06
canonical: https://one.ie/docs/quickstart
---
Three HTTP requests take you from no account at all to a working call against production. You need `curl` and nothing else. Every command and every response on this page was executed on 2026-09-06 — the three HTTP steps against `https://one.ie`, the SDK and CLI sections against the hosts each names. Three timed runs of the HTTP sequence took 5.5, 6.0 and 11.0 seconds end to end.

## 1. Mint a key

Post an empty body to the agent sign-up door. No credential, no signup form, no browser.

```bash
curl -s -X POST https://one.ie/api/auth/agent \
  -H 'Content-Type: application/json' \
  -d '{}'
```

```json
{
  "uid": "dc2187ae-31a2-...",
  "apiKey": "one-940e8634...",
  "wallet": null,
  "group": "group:dc2187ae-..."
}
```

Keep `apiKey`. It is a bearer credential for the actor the call created, and the door reports that caller's kind as `agent`. **It does not expire.** The route writes the key row with `valid_to` NULL, so it stays valid until it is revoked or rotated (`one.ie/web/src/lib/agent-key.ts` — `createKey`, `rotateKey`, which leaves the old key working for a five-minute grace). Save it to your shell:

```bash
export ONE_API_KEY='one-940e8634...'
```

## 2. Ask what the key can call

Every capability in ONE is a receiver name, not a URL. One door dispatches all of them: `POST /api/ask/<receiver>`. The receiver `meta:catalog` returns the list.

```bash
curl -s -X POST https://one.ie/api/ask/meta:catalog \
  -H "Authorization: Bearer $ONE_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"data":{}}'
```

```json
{
  "outcome": "result",
  "result": [
    {
      "receiver": "agent:enroll",
      "summary": "Self-enroll as an agent — mint identity + API key with no prior session",
      "effect": "ask",
      "auth": "public",
      "reversible": false,
      "idempotent": false
    }
  ],
  "signalId": "01a076858132a4244ff26aad",
  "receiver": "meta:catalog"
}
```

`result` holds 330 entries with that key — the whole registry. Two things about the request shape are load-bearing:

- **The payload goes under `data`.** Post the fields at the top level instead and the door refuses:

  ```json
  {"error":"envelope_missing","receiver":"meta:schema",
   "hint":"Wrap your payload in a \"data\" key: POST { \"data\": { ... } }",
   "got":["receiver"]}
  ```

- **Send the Bearer.** Without it this door answers `403` with content-type `application/problem+json`:

  ```json
  {"type":"about:blank","title":"forbidden","status":403,
   "detail":"direct substrate access not permitted"}
  ```

Each catalog row tells you what a call will do before you make it: `effect` (`ask` waits for an outcome, `signal` is fire-and-forget), `auth` (the caller class required), `reversible`, and `idempotent`. To get one receiver's exact request and response as JSON Schema, ask `meta:schema`:

```bash
curl -s -X POST https://one.ie/api/ask/meta:schema \
  -H "Authorization: Bearer $ONE_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"data":{"receiver":"world"}}'
```

## 3. Make a real call

`world` is the universal door — one of three receivers with no `family:` prefix. Send it tags and the world routes the signal: learned path first, then whoever staked on those tags, then the CEO agent.

```bash
curl -s -X POST https://one.ie/api/ask/world \
  -H "Authorization: Bearer $ONE_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"data":{"tags":["hello"]}}'
```

```json
{
  "outcome": "result",
  "result": { "ok": true, "routed": "ceo", "tags": ["hello"] },
  "signalId": "01a0768585ae3e9da3d24647",
  "receiver": "world"
}
```

`routed: "ceo"` means nobody had staked on `hello`, so the standing CEO workflow took it. That is the whole loop: mint, discover, call.

## Read `outcome`, not the HTTP status

A call that reached a receiver is **always HTTP 200**, whether it succeeded or not. The verdict is the `outcome` field:

| `outcome` | Means |
|---|---|
| `result` | The receiver ran and returned. `result` holds its answer. |
| `dissolved` | Nothing handled it — unknown receiver, or no handler wired. |
| `failure` | The receiver refused on authority. `result` names the action required and the caller's role. |

`failure` is narrower than it sounds: it is raised only when a receiver throws an authority refusal. Any other error a receiver throws or returns comes back as `outcome: "result"` with the detail inside `result` — so check `result.error` as well as `outcome`. Both shapes, from live calls. Note that the first is a receiver that ran and rejected its argument, which is not the same as `dissolved` below, where the door found no receiver at all:

```json
{"outcome":"result",
 "result":{"error":"unknown_receiver",
   "hint":"receiver must be a declared name; got nope:nope"},
 "signalId":"01a07698482d1730c7eb1244","receiver":"meta:schema"}

{"outcome":"failure",
 "result":{"error":"forbidden","receiver":"world:update-thing",
   "required":"manage_things","callerRole":"agent",
   "reason":"forbidden: not your thing",
   "hint":"Requires the admin role or higher in the target workspace.",
   "forbidden":true},
 "signalId":"01a076994ff426b41b8a2bc9","receiver":"world:update-thing"}
```

A fourth outcome, `timeout`, exists in the envelope type but is only reachable on the legacy external-processor path, which this repo does not configure. Asking an unknown receiver shows both facts at once — HTTP 200, and production naming the missing processor:

```json
{"outcome":"dissolved","signalId":"01a076919c7d7f930de61f98",
 "receiver":"tutor:explain","reason":"unknown_receiver",
 "hint":"unknown receiver; no external signal processor configured"}
```

Non-200 responses are transport refusals, and they come in more than one shape. The gate in front of the door answers RFC 7807 `application/problem+json` — that is the `403` above. The door's own refusals are plain `application/json` with an `error` field: `400 {"error":"envelope_missing", ...}`, `400 {"error":"validation","field":"receiver","hint":"Invalid input", ...}`, `401 {"error":"invalid_token", ...}`, `429 {"error":"rate_limited","retryAfter": ...}`.

The two sections below run those same three steps from a script and from a
shell. Read them once the curl version works, not instead of it.

## The same loop from a script — SDK

```bash
npm install @oneie/sdk
```

```ts
import { SubstrateClient } from "@oneie/sdk";

// baseUrl is not optional in practice — see below.
const one = new SubstrateClient({
  apiKey: process.env.ONE_API_KEY,
  baseUrl: "https://one.ie",
});

// Step 1, if you have no key yet:
// const { apiKey } = await one.authAgent({});

const cat = await one.ask("meta:catalog", {});
if (cat.kind === "result") console.log((cat.result as unknown[]).length);
// → 330

const sent = await one.ask("world", { tags: ["hello"] });
if (sent.kind === "result") console.log(sent.result);
// → { ok: true, routed: 'ceo', tags: [ 'hello' ] }
```

**Pass `baseUrl`.** The SDK resolves `http://localhost:4321` unless `NODE_ENV === "production"` or `ONEIE_API_URL` is set (`packages/sdk/src/urls.ts`). A script run in a normal shell without it will not reach production. This is the most common first-run failure.

**In a Cloudflare Worker, import by subpath:** `import { SubstrateClient } from "@oneie/sdk/client"`. The bare barrel is not Worker-safe — the package's own guidance is to reach it by subpath there. `@oneie/sdk` 0.14.13 publishes 29 subpaths.

## The same loop from a shell — CLI

```bash
export ONE_API_KEY='one-940e8634...'

npx oneie ask meta:catalog --data '{}'
npx oneie ask world --data '{"tags":["hello"]}'
```

The payload flag is `--data <json>` (or `--data -` to read JSON from stdin). A bare positional JSON argument is silently discarded and the call comes back `400 {"error":"validation", ...}`.

`one ask` defaults to `https://api.one.ie`, which is the same door with a shorter path shape — `POST /ask/<receiver>` instead of `/api/ask/<receiver>`. The CLI picks the right shape for whichever host it is pointed at, so you do not need `--api` for `ask`.

## Known limits

- **The key from step 1 never expires, and nothing narrows it for you.** Revoke or rotate it when you are done with it. For a narrower credential — scope capped at the actor's role, with an optional `expiresIn` in seconds — mint one with `world:create-key` against an actor you already own.
- **Step 1 is rate-limited to 10 requests per minute per IP.** Mint once and reuse the key; do not call it per request.
- **`POST /api/signal/<receiver>` is not a developer door.** It requires the server-to-server secret and answers `401` to an agent key. Use `ask` — 310 of the 330 receivers declare `effect: "ask"`.
- **The SDK derives its `kind` field client-side and it can disagree with the wire.** A response the server marked `"outcome":"dissolved"` arrives as `kind: "failure"`. Branch on `outcome` when the distinction matters.
- **`npx oneie` runs version 4.0.3.** The scoped package `@oneie/cli` is at 4.0.7. Install the scoped name if you need the newer one.
- **The dimension reads and verb commands — `one things`, `one signal`, `one mark`, `one fade` and the rest of that group — 404 against the CLI's default base.** They send the long `/api/*` path to `https://api.one.ie`, which serves only the short form. Add `--api https://one.ie` until that is fixed.
- **The published OpenAPI spec does not describe bearer authentication** and carries schemas for 181 of the 330 receivers. Treat `meta:catalog` and `meta:schema` as the current contract, not [/openapi.yaml](/openapi.yaml).

## Next

- [Receivers](/docs/receivers) — how a receiver name resolves, and what the 330 of them cover
- [API](/docs/api) — the door in full: envelope, outcomes, error shapes, rate limits
- [SDK](/docs/sdk) — typed `ask`, the six verbs, and the 29 subpath exports
- [CLI](/docs/cli) — all 110 commands, and which base URL each one talks to