Board Tool direct HTTP API
Build a server, script, or integration that calls Board Tool operations directly over HTTP.
The direct API is implemented and secured, but does not yet have MCP's one-URL onboarding. Obtain credentials and the exact token and API URLs for your environment from Budgee. No production URL is published here.
Authentication and consent
A signed-in Budgee user creates a narrowly scoped external credential through the authenticated boardToolAuthApi Firebase callable. Creation requires externalDataConsent: true, explicit boardIds, and explicit scopes.
const authApi = httpsCallable(
getFunctions(firebaseApp, "us-central1"),
"boardToolAuthApi",
);
const { data } = await authApi({
operation: "credentials.create",
params: {
type: "api-key",
externalDataConsent: true,
name: "Monthly reporting integration",
boardIds: ["board_123"],
scopes: ["board:read", "transactions:read", "reports:read"],
},
});The API key appears only when created or rotated. Store it in a secret manager. Rotation immediately invalidates the old key and its access tokens; listing cannot recover the key. Credential operations also include credentials.list, credentials.rotate, and credentials.revoke.
curl --request POST "$BOARD_TOOL_TOKEN_URL" \
--header "Content-Type: application/json" \
--data '{"grant_type":"api_key","api_key":"<budgee_sk_...>"}'{
"access_token": "<short-lived-token>",
"token_type": "Bearer",
"expires_in": 900,
"scope": "board:read transactions:read reports:read"
}Access tokens last 60–900 seconds; 900 is the default maximum. API-key exchange returns no refresh token, so exchange the key again when needed. MCP resource-bound tokens cannot be reused against this API and fail with detail code access-token-resource-mismatch.
Boards, scopes, and roles
Every request is limited by the token's user, exact board list, and exact scopes. Board Tool then rechecks the user's current role and whether the board is writable. Removing a role, revoking a credential, or making a board read-only takes effect while an access token still has time remaining.
Start read-only. Add a write scope only for its matching preview workflow, and add preview:apply only when the integration can present and preserve explicit human approval. Call board.capabilities after authentication instead of assuming every operation is available.
Request and response
curl --request POST "$BOARD_TOOL_HTTP_URL" \
--header "Authorization: Bearer $BOARD_TOOL_ACCESS_TOKEN" \
--header "Content-Type: application/json" \
--data '{
"boardId": "board_123",
"operation": "board.getSummary",
"params": {},
"requestId": "summary-2026-08-02-01"
}'boardIdis required for board-scoped operations and must be in the grant.operationis an exact registered operation name.paramsmust match the strict operation schema; unknown fields fail.requestIdis an optional, unique, non-secret correlation id.idempotencyKeyis for apply operations only.
{
"ok": true,
"version": "1.0",
"operation": "board.getSummary",
"requestId": "summary-2026-08-02-01",
"boardId": "board_123",
"boardFingerprint": "<sha256>",
"readBasis": {
"kind": "board-tool-read",
"coverage": "partial",
"capturedAt": "<server timestamp>"
},
"data": {}
}The illustration abbreviates readBasis. Accept the current response schema rather than comparing it byte-for-byte.
Pagination
For transactions.query, send pageSize from 1 to 200 with a filter and sort. When data.nextCursor is present, send it as params.cursor with the same filter and sort. Cursors are opaque and bound to that query and read-model generation. After read-model-transaction-cursor-stale, restart at page one without the cursor.
Preview, approval, apply, and readback
Create a preview with the operation matching the change. Show the human its previewId, previewHash, boardFingerprint, expiry, warnings, assumptions, proposed mutations, and apply instruction. After explicit approval, use the operation named by approvalCard.apply.operation.
{
"boardId": "board_123",
"operation": "previews.apply",
"requestId": "forecast-apply-1",
"idempotencyKey": "forecast-apply-6fdb6c88-5074-42a8-a73d-a9b6aee8c49f",
"params": {
"previewId": "<preview-id>",
"expectedPreviewHash": "<preview-hash>",
"expectedBoardFingerprint": "<preview-board-fingerprint>",
"confirmApply": true
}
}Use one unique idempotency key per logical apply. If transport fails before any structured result, retry only the same request, preview, and key. Never reuse a key for another preview. Poll queued work to a verified terminal state, and reconcile outcome-unknown or applied-unverified before any further apply.
Freshness
Direct HTTP uses bounded server reads. When the read model cannot prove current evidence, a read can fail with HTTP 412 and read-model-required, or return explicit catch-up status. Do not substitute cached client data. Wait for the authorized catch-up path and retry.
Errors, limits, and retries
HTTP errors use a stable envelope. error.code is the broad machine category; when present, error.details.code gives the specific recovery reason.
{
"ok": false,
"error": {
"code": "failed-precondition",
"message": "Preview has expired.",
"details": {
"code": "preview-expired"
}
}
}| Status | Meaning | Safe response |
|---|---|---|
| 400 | Invalid request, schema, operation, or cursor | Fix it; do not retry unchanged |
| 401 | Missing, invalid, expired, or rotated token | Exchange a valid credential |
| 403 | Scope, board, role, rollout, or revocation denial | Correct access; do not loop |
| 404 | Entity, preview, audit, or run absent | Re-resolve or recreate |
| 405 | Method is not POST | Use POST |
| 412 | Stale preview, changed source, missing confirmation, or unavailable read model | Follow details.code |
| 429 | Rate, plan, page, range, preview, or cost limit | Wait until resetAt or narrow |
| 503 | Temporary unavailability | Retry reads with bounded backoff |
| 500 | Redacted internal failure | Keep requestId; reconcile writes |
Default named-operation limits are 120 calls per minute for reads and 40 for writes, keyed to user, credential, board, and operation. Token exchange separately defaults to 30 per IP per minute and 120 per IP per 15 minutes. Plan or environment configuration can change these values, so returned max and resetAt fields are authoritative.
Never log bearer tokens, API keys, client secrets, or board content as a correlation id.