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.
getfacade/mcpMIT- 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
Quickstart
Issue a key
app.getfacade.aiAccountSettingsAPICreate 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.
Issue an agent keyRegister 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.
{ "mcpServers": { "getfacade": { "command": "npx", "args": ["-y", "@getfacade/mcp"], "env": { "GETFACADE_API_KEY": "your-key" } } } }Environment variables Variable Required Default GETFACADE_API_KEYYes —GETFACADE_API_BASE_URLNo https://api.getfacade.ai/api/v1Or 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.
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"}}}'
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_buildingcreate_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 /projectsupload_photoupload_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}/validationstart_designasynchronousstart_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}/rendersrefine_designasynchronousrefine_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_jobget_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_jobslist_jobs(kind?, limit? = 20) -> [{ job_id, kind, status, building_id, created_at }]Recent jobs across the account, unfinished first.
Calls
GET /historylist_designslist_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}/conceptsorder_estimateasynchronousorder_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}/estimatesorder_albumasynchronousorder_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/generateupscale_renderasynchronousupscale_render(render_id) -> { job_id, status }Enlarges a completed render. Costs tokens and runs asynchronously.
Calls
POST /renders/{render}/upscaleget_estimateget_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_lineadd_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}/itemsupdate_estimate_lineupdate_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_linedelete_estimate_line(estimate_id, line_id) -> { deleted }Removes one line from an estimate.
Calls
DELETE /estimates/{estimate}/items/{item}delete_renderdelete_render(render_id) -> { deleted }Deletes one render. Deleting the main render returns its design to draft.
Calls
DELETE /renders/{render}delete_designdelete_design(building_id, design_id) -> { deleted }Deletes one design with the renders under it.
Calls
DELETE /projects/{project}/concepts/{concept}delete_buildingdelete_building(building_id) -> { deleted }Deletes a building with everything under it. Tokens already spent are not refunded.
Calls
DELETE /projects/{project}list_token_packageslist_token_packages() -> [{ package, tokens, price, currency }]The packages this account can buy, with their price and token count.
Calls
GET /tokens/packagesbuy_tokensasynchronousbuy_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/purchaseget_balanceget_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/balancereport_problemreport_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
Authorizationheader. The MCP server reads it fromGETFACADE_API_KEYand 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_estimateandorder_albumreturn a job id and finish. Renders take minutes: pollGET /renders/{render}orGET /estimates/{estimate}until the state is terminal.- Photo validation is announced over a websocket an agent does not hold. Poll
GET /angles/{angle}/validationand readvalidation.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}/downloadis 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 | 120 | 2000 |
| Status polling and validation | 120 | 2000 |
| Render, estimate and album orders | 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-Keyis 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 422IDEMPOTENCY_KEY_REQUIREDand 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: trueon 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 409IDEMPOTENCY_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/mcpgenerates 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
apiwallet 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 answer says
chargedwhen the saved payment method covered it, orrequires_humanwith a checkout link a person opens. - The
apiscope is a separate wallet. The app's credits, the free tier included, are never spent by a key. - Credits already paid for in the app move into the
apiwallet from the API panel, on the web and in both mobile apps. Free credits stay where they are, and the transfer goes only this way. - Spend is metered per key, so each assistant's consumption is visible on its own.
- The pre-flight check is
GET /tokens/balance, fielddata.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)