# GetFacade agent API

Exterior design over MCP and HTTP. Each design is worked out for the country the building stands in: materials that are applicable there, manufacturer products that are actually sold there, and the technical build-up behind the surface. A render shows it on the photo of the house; the cost estimate prices it line by line, and the PDF album documents it for the crew that builds it. Every path below is an existing GetFacade endpoint, the same one the iOS, Android and web apps call. An agent key narrows who may call it, meters what it spends and stops at its cap.

| | |
|---|---|
| Base URL | `https://api.getfacade.ai/api/v1` |
| Authentication | `Bearer <agent key>` |
| Package | `@getfacade/mcp` |
| Runtime | Node.js 20+ |
| Transport | stdio (MCP), HTTPS (REST) |
| Specification | [OpenAPI 3.1, v1.0.0](https://getfacade.ai/openapi/agent-api.yaml) |

## Quickstart

1. **Issue a key.** The value is shown once and cannot be recovered, only replaced. The spend cap is set when the key is issued and enforced on every paid call.
2. **Register the MCP server.** One entry in the client config, then restart the client. Claude Desktop keeps it in claude_desktop_config.json; every other MCP client takes the same three fields.
3. **Or call the HTTP API.** The same key works as a bearer token. Requests and responses are JSON:API documents, where id is a top-level field and never sits inside attributes.

```json
{
  "mcpServers": {
    "getfacade": {
      "command": "npx",
      "args": ["-y", "@getfacade/mcp"],
      "env": { "GETFACADE_API_KEY": "your-key" }
    }
  }
}
```

```bash
curl -X POST https://api.getfacade.ai/api/v1/projects \
  -H "Authorization: Bearer $GETFACADE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"data":{"type":"project","attributes":{"name":"Maple Street 14"}}}'
```

### Environment variables

| Variable | Required | Default |
|---|---|---|
| `GETFACADE_API_KEY` | Yes | `—` |
| `GETFACADE_API_BASE_URL` | No | `https://api.getfacade.ai/api/v1` |

## Tools

Twenty-one tools. The MCP server holds no state and no rules of its own: each tool is one or more calls to the endpoints listed beside it, and every message the agent repeats is written by the API.

### `create_building`

```
create_building(name, goals?, construction_region?)
  -> { building_id, name }
```

Creates a building. The name is unique within the account and at most 50 characters; a duplicate is refused with 422.

Calls: `POST /projects`

### `upload_photo`

```
upload_photo(building_id, file_path, wait_for_validation? = true)
  -> { view_id, validation: { status, reason? } }
```

Registers a view, uploads the bytes to a presigned URL, confirms them and polls until the photo is accepted or rejected. Width, height and md5 are computed locally; the aspect ratio is derived by the server.

Calls: `POST /projects/{project}/angles → PUT (presigned) → POST /angles/{angle}/confirm → GET /angles/{angle}/validation`

### `start_design` (asynchronous)

```
start_design(building_id, view_id, prompt?, style_ids?, colors?,
             brand_selections?, render_effort?, seed?)
  -> { design_id, job_id, status, seed }
```

Designs the exterior on a chosen view and shows it on the photo: materials applicable in the building's country, products actually sold there, and the build-up behind the surface. Creates a design and queues the work, then returns the job id. The seed is optional: when omitted the server generates one and returns it.

Calls: `POST /projects/{project}/concepts → POST /concepts/{concept}/angles/{conceptAngle}/renders`

### `refine_design` (asynchronous)

```
refine_design(render_id | design_id + building_id, instruction,
              style_ids?, colors?, brand_selections?, render_effort?, seed?)
  -> { design_id, job_id, status, parent_render_id, seed }
```

Revises a finished design in words. The instruction is applied to the finished design, so anything it does not mention is kept. Revising a main view creates a new design, so the earlier one is never overwritten.

Calls: `GET /renders/{render} → POST /concepts/{concept}/angles/{conceptAngle}/renders (mode: refine)`

### `get_job`

```
get_job(job_id, kind: "render" | "album" | "estimate" = "render")
  -> { status, expected_seconds?, result_url?, error?, error_code? }
```

Reads the state of one render, estimate or album.

Calls: `GET /renders/{render} · GET /estimates/{estimate}`

### `list_jobs`

```
list_jobs(kind?, limit? = 20)
  -> [{ job_id, kind, status, building_id, created_at }]
```

Recent jobs across the account, unfinished first.

Calls: `GET /history`

### `list_designs`

```
list_designs(building_id)
  -> [{ design_id, note, has_main_render, main_render_id,
       main_render_url, renders }]
```

Designs of a building with their renders. This is where the render ids for an estimate or an album come from.

Calls: `GET /projects/{project}/concepts`

### `order_estimate` (asynchronous)

```
order_estimate(design_id, render_ids, currency?,
               measurement_system?, special_requirements?)
  -> { job_id, status }
```

Prices the design line by line, in materials and labour, at what the specified materials cost in the building's country. Currency and measurement system default to that country.

Calls: `POST /projects/{project}/concepts/{concept}/estimates`

### `order_album` (asynchronous)

```
order_album(design_id, render_ids, language?, include_blueprints?,
            include_estimate?, requirements?)
  -> { job_id, status }
```

Documents the design for the crew that builds it: the materials, the build-up of the facade, safety notes and the norms behind them. Requires a completed main render.

Calls: `POST /concepts/{concept}/album/generate`

### `upscale_render` (asynchronous)

```
upscale_render(render_id)
  -> { job_id, status }
```

Enlarges a completed render. Costs tokens and runs asynchronously.

Calls: `POST /renders/{render}/upscale`

### `get_estimate`

```
get_estimate(estimate_id)
  -> { status, currency, facade_area, materials_total, labor_total,
       grand_total, notes, lines: [{ line_id, section, name, quantity,
       unit, unit_price, line_total }] }
```

The estimate itself: totals, the assumptions behind them, and every line with quantity, unit and price. get_job reports an estimate's status, never its content.

Calls: `GET /estimates/{estimate}`

### `add_estimate_line`

```
add_estimate_line(estimate_id, section, name, quantity,
                  unit_price, unit?, category?)
  -> { line_id }
```

Adds one line to an estimate. Units come from the estimate's own measurement system.

Calls: `POST /estimates/{estimate}/items`

### `update_estimate_line`

```
update_estimate_line(estimate_id, line_id, name?, quantity?,
                     unit_price?, unit?, category?, section?)
  -> { line_id }
```

Changes one line of an estimate. Only the fields passed are touched; the server recomputes the totals.

Calls: `PATCH /estimates/{estimate}/items/{item}`

### `delete_estimate_line`

```
delete_estimate_line(estimate_id, line_id)
  -> { deleted }
```

Removes one line from an estimate.

Calls: `DELETE /estimates/{estimate}/items/{item}`

### `delete_render`

```
delete_render(render_id)
  -> { deleted }
```

Deletes one render. Deleting the main render returns its design to draft.

Calls: `DELETE /renders/{render}`

### `delete_design`

```
delete_design(building_id, design_id)
  -> { deleted }
```

Deletes one design with the renders under it.

Calls: `DELETE /projects/{project}/concepts/{concept}`

### `delete_building`

```
delete_building(building_id)
  -> { deleted }
```

Deletes a building with everything under it. Tokens already spent are not refunded.

Calls: `DELETE /projects/{project}`

### `list_token_packages`

```
list_token_packages()
  -> [{ package, tokens, price, currency }]
```

The packages this account can buy, with their price and token count.

Calls: `GET /tokens/packages`

### `buy_tokens` (asynchronous)

```
buy_tokens(package)
  -> { status, transaction_id, tokens, checkout_url?, detail }
```

Buys one package for this key's wallet. Requires a key issued with purchasing enabled, and never goes beyond what the key may still spend.

Calls: `POST /tokens/purchase`

### `get_balance`

```
get_balance()
  -> { balance, scope: "api", spend_cap, spent, remaining, is_admissible }
```

Agent wallet, the cap of this key and whether the next paid call will be admitted.

Calls: `GET /tokens/balance`

### `report_problem`

```
report_problem(message, category?,
               context?: { tool, endpoint, status_code, job_id,
                           expected, actual })
  -> { reference, message }
```

Reports a defect in this API: a field described here that never arrives, a refusal whose wording leaves no way forward, a result that does not match what was asked for. Free and accepted on an empty wallet; what comes back is a reference, not a reply.

Calls: `POST /feedback`

## Authentication and keys

- The key travels as a bearer token in the `Authorization` header. The MCP server reads it from `GETFACADE_API_KEY` and sends nothing else.
- The value is displayed once, at issue time, and only its hash is stored. Rotation means issuing a new key and revoking the old one.
- Every key carries a spend cap, enforced server-side before a call reaches a controller. Reaching the cap stops that key, not the account.
- Keys cannot manage keys: key endpoints are a human action and answer 403 to an agent key.
- Revocation takes effect immediately. Calls made with a revoked key answer 401.

## Asynchronous work and polling

- `start_design`, `order_estimate` and `order_album` return a job id and finish. Renders take minutes: poll `GET /renders/{render}` or `GET /estimates/{estimate}` until the state is terminal.
- Photo validation is announced over a websocket an agent does not hold. Poll `GET /angles/{angle}/validation` and read `validation.is_in_progress`; do not derive terminality from the status string yourself.
- A finished render and a finished album live at permanent public URLs: no signature, no expiry. The link can be handed to a person as the answer to “show me the result”. Being unsigned it asks nobody for permission, so it keeps working for whoever receives it and cannot be recalled.
- `GET /renders/{render}/download` is a different thing: a signed URL that expires in minutes and carries a filename. Use it to save the file, not to share it.

### Rate limits per key

An agent key gets its own buckets, separate from the human sessions of the same account, so a looping agent cannot eat the allowance of the person at the screen. Refusals are cheap: they are decided in middleware, before any database work.

| Scope | Per minute | Per hour |
|---|---|---|
| Reads and ordinary writes (`api`) | 120 | 2000 |
| Status polling and validation (`polling`) | 120 | 2000 |
| Render, estimate and album orders (`generation`) | 10 | 200 |

## Idempotency

A paid call creates a job, and the charge follows the job. Naming the call is what lets a retry return the same job instead of creating a second one.

- `Idempotency-Key` is required on every paid call made with an API key: starting or refining a design, upscaling a render, ordering an estimate or an album, regenerating an estimate. Without it the call answers 422 `IDEMPOTENCY_KEY_REQUIRED` and nothing is queued.
- Any value of 8 to 191 characters, one per order; a UUID is the usual choice. A new order takes a new value: two identical calls under two values are two designs.
- Repeating a call with the same value and the same body returns the original status and body, with `Idempotent-Replay: true` on the response. Nothing is queued and nothing is charged twice.
- The same value with a different body answers 422 `IDEMPOTENCY_KEY_REUSED`. A repeat that arrives while the first call is still running answers 409 `IDEMPOTENCY_IN_PROGRESS`: wait, then send the same call again.
- Any 4xx releases the value, so the same one can be sent again once the reason is fixed. Values are remembered for 24 hours, per account.
- `@getfacade/mcp` generates the value and retries under it on its own, so a tool call passes nothing.

## Errors

Failures arrive as JSON:API error documents. Laravel validation replies are not JSON:API-shaped and carry their text in message instead.

| Status | Code | Meaning | Retryable |
|---|---|---|---|
| `401` | `—` | The key is missing, revoked or expired. | No |
| `402` | `AGENT_CREDITS_EXHAUSTED` | The account has no `api`-scope credits left. | No |
| `402` | `AGENT_KEY_CAP_REACHED` | This key has spent its cap. Issue another key or raise the cap. | No |
| `403` | `—` | This endpoint is not available to API keys. The agent API covers buildings, photos, designs, renders, estimates, albums and the API wallet. Account, sign-in and billing settings are changed by a person signed in to the app. | No |
| `403` | `AGENT_PURCHASE_NOT_ALLOWED` | This key was issued without permission to buy tokens. | No |
| `403` | `AGENT_PURCHASE_EXCEEDS_CAP` | The purchase would take the key past its spend cap. | No |
| `409` | `IDEMPOTENCY_IN_PROGRESS` | The first call carrying this `Idempotency-Key` has not answered yet. Wait and send the same call again. | Yes |
| `422` | `—` | The request was understood and refused: a duplicate building name, a rejected photo, an album ordered before the main render finished. | No |
| `422` | `IDEMPOTENCY_KEY_REQUIRED` | A paid call from an API key with no `Idempotency-Key` header. Nothing was queued; send it again with one. | No |
| `422` | `IDEMPOTENCY_KEY_REUSED` | This `Idempotency-Key` was used for a different request. Use a new value for a new order. | No |
| `429` | `—` | The key's own rate bucket is exhausted. Back off, do not retry in a tight loop. | Yes |

The human-readable text is written by the API, in the caller's language. Display errors[].detail as it stands rather than composing your own message.

## Billing and admission

- Paid calls draw on `api`-scope credits, and admission looks at that balance alone: every key pays in credits.
- An active Pro Plan tops the `api` wallet up to 1,000 credits once per billing period. Past that, credits are bought.
- A key can refill its own wallet only if it was issued with purchasing enabled, and only up to what it may still spend, so a purchase never lifts the spend cap.
- The `api` scope is a separate wallet. The app's credits, the free tier included, are never spent by a key.
- Spend is metered per key, so each assistant's consumption is visible on its own.
- The pre-flight check is `GET /tokens/balance`, field `data.attributes.agent.is_admissible`. The block is present only for agent keys, and the flag mirrors the admission middleware exactly. Read it instead of comparing the balance with the cap.
- Admission is decided before any work is queued, so a refused call spends nothing.

## Colour and brand tokens

start_design takes two independent lists of at most ten entries each. Order carries the 60/30/10 role: the first entry is the dominant wall colour.

### `colors`

| Token | Meaning |
|---|---|
| `palette:1` | A curated GetFacade scheme, by id. |
| `#8A8F7D` | A free-form colour, six hexadecimal digits. |
| `paint:412` | A manufacturer chip, in the two-segment form kept for compatibility. |

### `brand_selections`

| Token | Meaning |
|---|---|
| `siding:brand:12` | Any product of that manufacturer in that category. |
| `siding:line:40@double-4-dutchlap` | One line, on one geometry. |
| `siding:product:88@double-4-dutchlap` | One product, fully specified. |
| `paint:brand:3` | Any colour of that paint brand. |
| `paint:product:412` | One paint chip. |

The grammar is category:level:id[@value][.value]. The part after @ carries geometry value slugs, which are unique within their category, so the axis they belong to is a lookup rather than something the token spells out. An unknown token is refused with 422, never silently ignored.

## End-to-end session

One building, one photo, one design, then the two documents. The prompt that produces it:

> Create a building called Maple Street 14, upload ./front.jpg as its view, and start a design with warm grey walls and white trim. Order the estimate and the album for the result.

```
create_building(name: "Maple Street 14")
  -> { building_id: "0f8c…" }

upload_photo(building_id: "0f8c…", file_path: "./front.jpg")
  -> { view_id: "41ab…", validation: { status: "approved" } }

start_design(building_id: "0f8c…", view_id: "41ab…",
             colors: ["#8A8F7D", "#F2F0EB"],
             brand_selections: ["siding:line:40@double-4-dutchlap"])
  -> { design_id: "7d21…", job_id: "b933…", status: "queued" }

get_job(job_id: "b933…")
  -> { status: "completed", result_url: "https://…" }

refine_design(render_id: "b933…",
              instruction: "put a canopy over the front door")
  -> { design_id: "9e44…", job_id: "c07f…", status: "queued" }

order_estimate(design_id: "9e44…", render_ids: ["c07f…"])
order_album(design_id: "9e44…", render_ids: ["c07f…"], include_estimate: true)
```

## Resources

- [OpenAPI 3.1 specification (YAML)](https://getfacade.ai/openapi/agent-api.yaml)
- [@getfacade/mcp on npm](https://www.npmjs.com/package/@getfacade/mcp)
- [Issue an agent key](https://app.getfacade.ai/account/agent-keys)
- [Contact us](https://getfacade.ai/contact)
