# Integrate with the Economico API over HTTP

Follow this procedure to call Economico's REST API directly. Do not open an MCP
session. MCP, REST and the CLI are three adapters over one registry. This page
is the HTTP adapter.

The human walkthrough is [Direct API quickstart](https://economi.co/docs/direct-api/).
For the object model, read [agent concepts](https://economi.co/docs/concepts/agent/index.md).
Every endpoint and schema is in the [complete reference](https://economi.co/docs/api-reference/index.md).
Transport status codes are [the request reference](https://economi.co/docs/api/).
Command behaviour is one page per tool under the developer docs. When this page
and a live `GET /v1/catalog/commands/{name}` disagree, stop and use the catalog.

## Stop

Stop, and do not send the request, when any of these are true.

- The host is not `https://economi.co`. Every other hostname is a 404.
- The token came from a prompt, a log, a URL, or a source file. Ask for a new
  grant. Do not copy the token into those places.
- The business slug is `economico`. That is Economico's own company
  (`did:web:economi.co`). Do not read or write it from this procedure.
- You are about to `POST /v1/commands` and `GET /v1/business` did not return
  `demo: true`, and the operator did not name this slug as a disposable sandbox
  in the current task.
- A response is `401`. The bearer is missing, expired or invalid. Do not retry
  the same token. Re-authorize.
- A response is `403` with `code` `forbidden` or `demo_business`. Do not ask for
  a wider grant and retry. Read `GET /v1/catalog/permissions` and the `message`.
- A response is `409` with `code` `idempotency_conflict`. Do not mint a new key
  for the same intent. Read the message. The key already names a different
  command, input, or `source_document_id`.
- You do not yet have `result.documentId` or `result.id` for a later step that
  needs it. Do not invent an id.

A lost response is not a failure. Repeat the same `POST /v1/commands` body,
including the same `idempotency_key`. A new key is a new command.

## Constants

```text
HOST=https://economi.co
RESOURCE=https://economi.co/mcp
SCOPE=mcp
```

Send `Authorization: Bearer <access_token>` on every `/v1` call. Send
`Accept: application/json`. Do not put the token in a query string.

A token minted for `https://economi.co/mcp/chatgpt` cannot call `/v1` or
`/mcp`. This procedure uses `RESOURCE` above.

There is no static API key and no GraphQL API.

## 1. Authorize

The only OAuth scope is `mcp`. It names the resource. It does not grant a
command. Requesting any other scope on the headless token endpoint returns
`invalid_scope`.

Read the discovery documents and use their endpoints. Do not hard-code a host
other than `HOST`.

```sh
curl -sS "$HOST/.well-known/oauth-authorization-server"
curl -sS "$HOST/.well-known/oauth-protected-resource/mcp"
```

Use an approved human account, or your own AgentID identity through the
[AgentID branch below](#agentid-sign-in-or-create-a-business). Both use the same
Economico OAuth resource and business consent.

Interactive clients use the authorization-code grant with PKCE. Only `S256` is
accepted. Plain PKCE is refused. The authorize request the server accepts
carries:

```text
response_type=code
client_id=<registered client id>
redirect_uri=<the redirect URI registered for that client>
state=<unguessable value you check on return>
code_challenge=<S256 challenge of a verifier you keep>
code_challenge_method=S256
resource=https://economi.co/mcp
scope=mcp
```

Complete sign-in and consent for the permitted businesses. Exchange the code at the token
endpoint (`POST /oauth/token`, `application/x-www-form-urlencoded`):

```text
grant_type=authorization_code
code=<code>
client_id=<same client id>
redirect_uri=<same redirect URI>
code_verifier=<the PKCE verifier>
```

The JSON body contains `access_token` and `refresh_token`. Access tokens live
3600 seconds. Interactive refresh tokens live 7 days. Refresh with:

```text
grant_type=refresh_token
client_id=<same client id>
refresh_token=<refresh_token>
```

A public client is registered with `token_endpoint_auth_method` `none`,
`grant_types` `authorization_code` and `refresh_token`, and `response_types`
`code`. `POST /oauth/register` is limited to 10 requests per 10 seconds per
address. A 429 from that path can still carry `Retry-After: 60`, because any
429 that did not set its own delay gets that header. Wait, and do not register
in a loop.

### AgentID: sign in or create a business

Use this branch when you control an AgentMail inbox and need your own agent
identity. AgentID signs you in; Economico issues the OAuth token used by REST.

1. Start your client's PKCE authorization flow, or use the [CLI](https://economi.co/docs/cli/):
   `economico login --server https://economi.co --local`. Keep that flow running
   and choose **Continue with AgentID**. A standalone web sign-in does not give
   your API client a token.
2. Use your configured AgentMail inbox and bearer key with `app_connect` and
   `app_share_owner`. Set `AGENTMAIL_INBOX_ID` and `AGENTMAIL_API_KEY` from trusted
   configuration. Check that the pending sign-in's final origin is exactly
   `https://auth.agentid.com` and the app is Economico. Read its `auth_token`
   privately into `AGENTID_AUTH_TOKEN`, then authorize the waiting sign-in:

   ```sh
   agentmail inboxes authorize \
     --inbox-id "$AGENTMAIL_INBOX_ID" \
     --auth-token "$AGENTID_AUTH_TOKEN" \
     --accept-disclosure true
   ```

   Send the AgentMail bearer key only to `https://api.agentmail.to`. Return to
   the same browser to complete activation and the Economico callback. Follow
   the [full AgentID procedure](https://economi.co/docs/authentication/index.md#sign-in-as-an-agent-with-agentid)
   and [AgentMail's sign-in guide](https://docs.agentmail.to/agentid-sign-in)
   for setup, pending-key recovery and expiry handling.
3. For an existing business, obtain the human owner's membership grant for your
   AgentID principal. Your user ID is at `https://economi.co/account/members`.
   A missing business is an access question; do not create duplicate books.
4. For a new business, choose **Email approval code** and have the verified human
   owner approve through the separate signup-code step. Use an approved private
   channel for that signup-only code. It cannot sign in as the human. The approval
   covers one new-company offer for ten minutes. Finish the Solo agreement:
   the human is the sole owner and you receive bookkeeper access. If approval
   expires, renew it and resume the same offer. The owner can upgrade separately.
5. Finish the original OAuth consent. Continue only after your client has an
   Economico access token and `GET /v1/businesses` lists the intended business.
   Verify effective permissions before writing. AgentID identity tokens and
   AgentMail keys are not Economico API tokens; human-only controls remain with
   the owner.

### Unattended private-key clients

Headless clients do not use a refresh token. `POST /oauth/headless/token`
(`application/x-www-form-urlencoded`) accepts only:

```text
grant_type=client_credentials
client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
client_assertion=<signed JWT>
scope=mcp
```

`scope` may be omitted. Any value other than `mcp` is `invalid_scope`. The
assertion is a JWT with `kid`, `alg` `RS256` or `ES256`, `iss` and `sub` both
the `client_id`, `aud` exactly `https://economi.co/oauth/headless/token`,
numeric `iat` and `exp` at most 60 seconds apart, and a unique `jti`. A reused
`jti` is refused. The response is `access_token`, `token_type` `bearer`,
`expires_in` `3600`, `scope` `mcp`, and `resource`. Mint a new assertion when
the access token expires.

Creating the headless client is a different procedure: verified-email signup at
`/oauth/agent/signup/start`, `verify`, `offer`, and `accept`. The continuation
bearer from signup is not a token for an existing business. Follow
[authentication](https://economi.co/docs/authentication/) for that ladder. Do
not treat this page as the signup guide.

Store the access token in `ECONOMICO_TOKEN` in the process environment for the
steps below. Set `HOST=https://economi.co` in your shell. Do not write the token
to tracked repository files.

## 2. Choose a business

`GET /v1/businesses` needs a bearer and no business header. `audience` is
`accessible` (default) or `public`. `after` and `limit` page the list.

```sh
curl -sS "$HOST/v1/businesses" \
  -H "Authorization: Bearer $ECONOMICO_TOKEN"
```

Pick a slug from that list. Call it `ECONOMICO_BUSINESS`. If the slug is
`economico`, stop.

```sh
curl -sS "$HOST/v1/business" \
  -H "Authorization: Bearer $ECONOMICO_TOKEN" \
  -H "X-Economico-Business: $ECONOMICO_BUSINESS"
```

`GET /v1/business` is the profile read (`action` `get`). Use it only when
`demo` is `true`, or when the operator named this slug as a disposable sandbox.
A demo's `name` ends in `(demo)`. Report its figures as fictional.

These routes do not take `X-Economico-Business`:

- `GET /v1/businesses`
- `GET /v1/vendor-catalog` and the other `/v1/vendor-catalog/…` routes
- `GET /v1/openapi.json` when you want the connection document

Every other `/v1` route requires the header. A missing header is HTTP 400:

```json
{"code":"validation","message":"Set X-Economico-Business to the business for this request"}
```

A slug this connection cannot use is HTTP 403:

```json
{"code":"forbidden","message":"Access to this business is unavailable"}
```

That message does not say whether the slug exists. A headless token's
`business_id` claim does not replace the header.

`GET /v1/openapi.json` with no header describes connection-wide discovery. The
same path with the header describes that business's permitted operations. The
public file `GET /openapi.json` is a reference, not a grant.

## 3. Read what this token may do

Permission is the intersection of live membership, the application grant, and
the token ceiling. Presets (`reports`, `reader`, `bookkeeper`, `administrator`,
`owner`) expand to statements. Read the statements. Do not assume a preset.

```sh
curl -sS "$HOST/v1/catalog/permissions" \
  -H "Authorization: Bearer $ECONOMICO_TOKEN" \
  -H "X-Economico-Business: $ECONOMICO_BUSINESS"
```

```sh
curl -sS "$HOST/v1/catalog" \
  -H "Authorization: Bearer $ECONOMICO_TOKEN" \
  -H "X-Economico-Business: $ECONOMICO_BUSINESS"
```

`GET /v1/catalog` returns permitted command `name` and `description` only.
Describe one command before you send it. The body is that command's input
schema, result schema, examples, `sourceDocumentKinds`, and `credential`.

```sh
curl -sS "$HOST/v1/catalog/commands/parties.create" \
  -H "Authorization: Bearer $ECONOMICO_TOKEN" \
  -H "X-Economico-Business: $ECONOMICO_BUSINESS"
```

If `credential` is `browser`, stop. OAuth cannot run it. If the name is absent
from `GET /v1/catalog`, stop. Do not send the command.

`GET /v1/reports` is the report catalog. Report parameters that are not in that
kind's schema are refused.

## 4. First read

This uses `POST /v1/reports` to read a report. It does not change the books.

```sh
curl -sS "$HOST/v1/reports" \
  -H "Authorization: Bearer $ECONOMICO_TOKEN" \
  -H "X-Economico-Business: $ECONOMICO_BUSINESS" \
  -H "Content-Type: application/json" \
  -d '{"kind":"balance_sheet","currency":"USD"}'
```

`kind` is required. `currency` and `as_of` are optional on `balance_sheet`.
`currency` `USD` is a native-ledger view. Money amounts in the JSON are decimal
integer strings of minor units, not JSON numbers. An empty ledger does not
establish cash or runway. Stop if the status is not 200.

A `401` body keeps the OAuth `WWW-Authenticate` challenge and adds `code`
`unauthorized`, `message`, and `hint`. Follow `hint`. Do not retry.

## 5. Sandbox money loop

Do this only after the stop check in section 2. The loop records a fictional
customer, a draft contract, acceptance, one billing record, and an income
statement. It does not move money and it does not email the customer.

There is no `invoices.send` and no `POST /v1/invoices`. A bill is
`contracts.record` with `phase` `billing`. `messages.send` delivers a message
to another Economico business that already shares an agreement or a connection.
On a demo, `messages.send` with `to` set is HTTP 403 `demo_business`. Do not
call it in this loop.

The command bodies below are the public recipe "Customer on a monthly
subscription plan, billed in advance", which the ledger replays. Amount `4900`
is USD 49.00. Substitute `<…>` from the receipt of the named step. Each
`idempotency_key` is stable for that step. `effective_at` is UTC, ends in `Z`,
and has at most millisecond precision.

Before each command, `GET /v1/catalog/commands/<name>` and confirm the body
matches that schema. If it does not, stop.

Shared headers for every command:

```text
Authorization: Bearer $ECONOMICO_TOKEN
X-Economico-Business: $ECONOMICO_BUSINESS
Content-Type: application/json
```

POST each body to `$HOST/v1/commands`. A 200 body with `replayed` `false` is
the first accept. The same body again returns `replayed` `true` and the same
`commandId`. `no_change` is also HTTP 200 and has no receipt. Read `code`
before `result`.

### 5.1 Store the order form

`documents.receive` is `POST /v1/documents` (the envelope has no `name`) or
`POST /v1/commands` with `name` `documents.receive`. Use `/v1/commands` here.

```json
{
  "name": "documents.receive",
  "idempotency_key": "direct-api:order-form",
  "effective_at": "2026-07-01T00:00:00Z",
  "input": {
    "kind": "source_document",
    "contentBase64": "QWNtZSBBbmFseXRpY3MgLSBQcm8gcGxhbiBvcmRlciBmb3JtClBsYW46IFBybyBtb250aGx5LCAkNDkuMDAgcGVyIG1vbnRoLCBiaWxsZWQgbW9udGhseSBpbiBhZHZhbmNlLgpTZXJ2aWNlIHN0YXJ0OiAyMDI2LTA3LTAxLiBNb250aCB0byBtb250aDsgY2FuY2VsIGFueSB0aW1lIGVmZmVjdGl2ZSBhdCB0aGUgZW5kIG9mIHRoZSBwYWlkIG1vbnRoLgpBY2NlcHRlZCB2aWEgY2hlY2tvdXQgb24gMjAyNi0wNy0wMSBieSBvcHNAYWNtZS5leGFtcGxlLg==",
    "externalId": "stripe:checkout:cs_acme_pro"
  }
}
```

Keep `result.documentId` as `<order-form.documentId>`. The source is fictional
sandbox evidence; it does not assert a real Stripe checkout or invoice.

### 5.2 Create the customer

`parties.create` posts no money.

```json
{
  "name": "parties.create",
  "idempotency_key": "direct-api:customer",
  "effective_at": "2026-07-01T00:00:00Z",
  "input": {
    "partyId": "cus_acme_direct_api",
    "name": "Acme Analytics",
    "categories": ["customer"]
  }
}
```

The published recipe uses `partyId` `cus_acme`. This procedure uses
`cus_acme_direct_api` so a second pass does not collide with a business that
already ran that recipe. Use this id in `parties` below. Replay this same body
once. `replayed` must be `true`. Then continue. A second key with the same
`partyId` is HTTP 409 `conflict`, not a replay.

### 5.3 Register the bank account the later payment would use

This step does not pay the invoice. It is the account id the recipe's payment
uses, so the books have a place a later `payments.record` can name. Skip the
payment in this procedure.

```json
{
  "name": "accounts.register",
  "idempotency_key": "direct-api:bank",
  "effective_at": "2026-07-01T00:00:00Z",
  "input": {
    "financialAccountId": "bank_main_direct_api",
    "name": "Operating bank",
    "currency": "USD",
    "kind": "checking"
  }
}
```

Confirm `accounts.register` is in the catalog before sending. If it is not,
skip this step and continue. Do not invent another account command.

### 5.4 Create the right

```json
{
  "name": "activities.create",
  "idempotency_key": "direct-api:access-right",
  "effective_at": "2026-07-01T00:00:00Z",
  "input": {
    "definition": {
      "model": "priced-rights-v1",
      "name": "Pro platform access",
      "terms": {
        "price": {"type": "integer", "unit": "USD", "default": "4900"},
        "start": {"type": "date"},
        "end": {"type": "date"},
        "currency": {"type": "text", "default": "USD"}
      },
      "facts": {},
      "ledgers": {
        "money": {
          "classification": "gaap",
          "unitKind": "currency",
          "unitCode": "USD",
          "decimals": 2,
          "accountTemplate": "gaap_startup"
        }
      },
      "price": {
        "treatment": "priced",
        "calculation": {"type": "fixed", "amount": "term:price"},
        "currencyTerm": "currency"
      },
      "recurringValue": {
        "interval": "monthly",
        "startTerm": "start",
        "endTerm": "end"
      },
      "economics": {
        "kind": "service",
        "direction": "provide",
        "billing": {
          "timing": "advance",
          "schedule": {
            "startTerm": "start",
            "endTerm": "end",
            "everyMonths": 1,
            "timing": "start",
            "offsetDays": 0
          }
        },
        "recognition": {
          "timing": "ratable",
          "schedule": {
            "startTerm": "start",
            "endTerm": "end",
            "everyMonths": 1,
            "timing": "end",
            "offsetDays": 0,
            "serviceEvidence": "elapsed_time"
          }
        },
        "classification": {
          "monetaryLedger": "money",
          "revenueCategory": "subscription",
          "product": "pro"
        }
      }
    }
  }
}
```

Keep `result.documentId` as `<access-right.documentId>`.

### 5.5 Create the template

```json
{
  "name": "templates.create",
  "idempotency_key": "direct-api:plan",
  "effective_at": "2026-07-01T00:00:00Z",
  "input": {
    "definition": {
      "name": "Pro monthly plan",
      "plan": "on_sale",
      "parties": ["customer"],
      "ledgers": {
        "money": {
          "classification": "gaap",
          "unitKind": "currency",
          "unitCode": "USD",
          "decimals": 2,
          "accountTemplate": "gaap_startup"
        }
      },
      "activities": [
        {
          "key": "access",
          "activityDocumentId": "<access-right.documentId>",
          "partyRole": "customer",
          "ledgers": {"money": "money"}
        }
      ],
      "model": "priced-rights-v1"
    }
  }
}
```

Keep `result.documentId` as `<plan.documentId>`.

### 5.6 Create the draft contract

`ledgerBindings.money` is the USD ledger id: `led_` plus ISO 4217 numeric 840,
zero-padded, `led_00000000000000000000000840`. `GET /v1/ledgers` returns
`{ items, nextCursor }`. Each item has `ledgerId`. Follow `nextCursor` as
`after` until it is null. Continue only if some item's `ledgerId` is that USD
id. If none is, stop.

```sh
curl -sS "$HOST/v1/ledgers" \
  -H "Authorization: Bearer $ECONOMICO_TOKEN" \
  -H "X-Economico-Business: $ECONOMICO_BUSINESS"
```

```json
{
  "name": "contracts.create",
  "idempotency_key": "direct-api:contract",
  "effective_at": "2026-07-01T00:00:00Z",
  "input": {
    "templateDocumentId": "<plan.documentId>",
    "parties": {"customer": "cus_acme_direct_api"},
    "terms": {"access": {"start": "2026-07-01", "end": "2027-07-01"}},
    "ledgerBindings": {"money": "led_00000000000000000000000840"},
    "sourceDocumentIds": ["<order-form.documentId>"]
  }
}
```

Keep `result.id` as `<contract.id>` and `result.documentId` as
`<contract.documentId>`. The contract is a draft. It has not billed.

Read it back:

```sh
curl -sS "$HOST/v1/subjects/contract/<contract.id>" \
  -H "Authorization: Bearer $ECONOMICO_TOKEN" \
  -H "X-Economico-Business: $ECONOMICO_BUSINESS"
```

Continue only if `status` is `draft`.

### 5.7 Accept

```json
{
  "name": "contracts.accept",
  "idempotency_key": "direct-api:accept",
  "effective_at": "2026-07-01T00:00:00Z",
  "input": {
    "contractId": "<contract.id>",
    "sourceFactId": "stripe:checkout:cs_acme_pro",
    "effectiveAt": "2026-07-01T00:00:00Z",
    "evidence": {
      "acceptance": [{"kind": "signed_doc", "ref": "<order-form.documentId>"}]
    },
    "expectedDocumentId": "<contract.documentId>",
    "expectedStatus": "draft"
  }
}
```

`effectiveAt` inside `input` is the command's own field. The envelope
`effective_at` is the same instant here. Do not drop either.

### 5.8 Record the July invoice

This is the billing record. It does not collect the cash.

```json
{
  "name": "contracts.record",
  "idempotency_key": "direct-api:bill-july",
  "effective_at": "2026-07-01T00:00:00Z",
  "input": {
    "contractId": "<contract.id>",
    "activityKey": "access",
    "occurrenceKey": "schedule:2026-07-01:2026-08-01",
    "sourceFactId": "stripe:invoice:in_acme_2026_07",
    "effectiveAt": "2026-07-01T00:00:00Z",
    "facts": {},
    "evidence": {
      "billing": [{"kind": "stripe_object", "ref": "stripe:invoice:in_acme_2026_07"}]
    },
    "phase": "billing",
    "period": {"from": "2026-07-01T00:00:00Z", "through": "2026-08-01T00:00:00Z"}
  }
}
```

Repeat this same body once. `replayed` must be `true`. If you change
`occurrenceKey` or `sourceFactId` and keep `idempotency_key`, the response is
`idempotency_conflict`. Stop.

### 5.9 Read the income statement

```sh
curl -sS "$HOST/v1/reports" \
  -H "Authorization: Bearer $ECONOMICO_TOKEN" \
  -H "X-Economico-Business: $ECONOMICO_BUSINESS" \
  -H "Content-Type: application/json" \
  -d '{"kind":"income_statement","currency":"USD","from":"2026-07-01","until":"2026-08-01T00:00:00Z"}'
```

On a fresh USD sandbox with only this walkthrough's records, require HTTP 200
and `revenue.total` `"0"`, `expenses.total` `"0"`, and `net_income` `"0"`.
The bill records a receivable and deferred revenue; it does not earn revenue.
A balance sheet at `as_of` `2026-08-01T00:00:00Z` shows trade receivables
(account 1120) and deferred revenue (account 2150), each `"4900"`. No cash
was recorded. A demo with existing postings can have other balances; use a
fresh disposable business to reproduce these totals.

The recipe's later recognition step, which this procedure does not run,
is what earns `"4900"` of revenue. Money fields are integer strings.

Recording the payment (`payments.record`) and the recognition phase are further
steps of the same recipe. Do not add them unless the operator asked. They post
cash and earned revenue. Describe each command first.

Platform-plan purchases use `POST /v1/platform/purchases`, with a separate
MPP/x402 payment URL. Follow the [API reference](https://economi.co/docs/api/)
for that flow; it is separate from recording a customer invoice here.

## 6. Errors

Refusals are JSON `code`, `message`, and sometimes `details`.

| Status | code you will see | What to do |
| --- | --- | --- |
| 200 | absent, or `no_change` | Success. `no_change` has no receipt. |
| 400 | `validation` | Fix the body from the current command schema. Do not retry unchanged. |
| 401 | `unauthorized` | Re-authorize. Do not reuse the token. |
| 402 | `plan_required` | The write was not stored. Read `details.reason`. Do not retry immediately. |
| 403 | `forbidden`, `demo_business` | Stop. Do not widen the grant automatically. |
| 404 | `not_found` | Unknown route, or unknown object. A missing read inside an authorized business is often JSON `null` rather than 404. |
| 409 | `idempotency_conflict`, `conflict` | Recover the existing outcome. Do not change the key and resend. |
| 415 | `unsupported_media_type` | Send `application/json` on `/v1/commands`. |
| 429 | `rate_limited` | Wait for `Retry-After` when the limit is a short budget. See limits. |
| 503 | `busy` | The request did not start. Retry the same body after `Retry-After`. |

`plan_required` for a Solo event allowance includes `details.reason`
`solo_limit`, `included`, `used`, and `requested` as integer strings, plus
`from` and `through`. A demo refuses recording past 2000 events with
`demo_business`. Reads stay available.

## 7. Limits that exist

Authenticated `/v1` does not advertise a per-token request quota. Do not invent
one. These budgets do exist:

- Dynamic client registration: 10 per 10 seconds per address.
- Unauthenticated login and OTP: 10 per 60 seconds.
- Anonymous `GET /public/…`: 60 per 60 seconds per address. Responses advertise
  `RateLimit-Policy: "public";q=60;w=60`. A refusal also sends `Retry-After: 60`.
- `/e/{slug}/` browser paths: 240 per 60 seconds per address. This does not
  apply to `/v1`.
- Vendor catalog: 120 per 60 seconds per principal. Exhaustion is HTTP 429
  `rate_limited` with `Retry-After: 60`.
- A vendor-plan command that spends the catalog budget from `POST /v1/commands`
  is HTTP 429 `rate_limited` with `Retry-After: 5`.
- `busy` is HTTP 503 with `Retry-After: 2`. Retry the same body.
- Solo without a paid agreement: 100 billable events in the business's monthly
  period. Crossing it is HTTP 402 `plan_required` and writes nothing.
- Document bytes: 1 GiB of distinct stored bytes per business. Sending the same
  bytes again does not add to the total.
- JSON bodies over 256 KiB, and multipart uploads, need `Content-Length`. Over
  the shared upload budget, after authentication, is HTTP 503 `busy`.
- Logo deletes and replacements of published images: 50 per business per UTC
  day, HTTP 429 `logo_churn_exceeded`. The handler does not set `Retry-After`
  for that code, so the router adds `Retry-After: 60`. Sleeping 60 seconds does
  not reset the daily cap. Do not retry on that header.

Successful responses do not report remaining quota.

## 8. Credential safety

- Keep the access token, the refresh token, the PKCE verifier, and the headless
  private key only in the process that sends the request.
- Do not send a token to any origin other than `https://economi.co`.
- Do not log `Authorization`, OTP codes, or client assertions.
- Revoke a grant from account connections when the task is finished. A `403`
  after revocation is expected. Do not register a second client to get past it.
- Rehearse on a demo company (`GET /oauth/business/demo` in a browser, then
  connect that slug) or on a disposable sandbox the operator named. Never on
  slug `economico`.

For command behavior, inputs and receipts: https://economi.co/docs/command-reference/index.md
For report purpose and parameter meanings: https://economi.co/docs/report-reference/index.md
For terminal execution: https://economi.co/docs/cli/index.md
