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

  1. 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 key
  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.

    claude_desktop_config.json
    {
      "mcpServers": {
        "getfacade": {
          "command": "npx",
          "args": ["-y", "@getfacade/mcp"],
          "env": { "GETFACADE_API_KEY": "your-key" }
        }
      }
    }
    Environment variables
    VariableRequiredDefault
    GETFACADE_API_KEYYes
    GETFACADE_API_BASE_URLNohttps://api.getfacade.ai/api/v1
  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.

    shell
    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_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.

    CallsPOST /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.

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

  • start_designasynchronous
    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.

    CallsPOST /projects/{project}/concepts → POST /concepts/{concept}/angles/{conceptAngle}/renders

  • refine_designasynchronous
    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.

    CallsGET /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.

    CallsGET /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.

    CallsGET /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.

    CallsGET /projects/{project}/concepts

  • order_estimateasynchronous
    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.

    CallsPOST /projects/{project}/concepts/{concept}/estimates

  • order_albumasynchronous
    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.

    CallsPOST /concepts/{concept}/album/generate

  • upscale_renderasynchronous
    upscale_render(render_id)
      -> { job_id, status }

    Enlarges a completed render. Costs tokens and runs asynchronously.

    CallsPOST /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.

    CallsGET /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.

    CallsPOST /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.

    CallsPATCH /estimates/{estimate}/items/{item}

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

    Removes one line from an estimate.

    CallsDELETE /estimates/{estimate}/items/{item}

  • delete_render
    delete_render(render_id)
      -> { deleted }

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

    CallsDELETE /renders/{render}

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

    Deletes one design with the renders under it.

    CallsDELETE /projects/{project}/concepts/{concept}

  • delete_building
    delete_building(building_id)
      -> { deleted }

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

    CallsDELETE /projects/{project}

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

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

    CallsGET /tokens/packages

  • buy_tokensasynchronous
    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.

    CallsPOST /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.

    CallsGET /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.

    CallsPOST /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.

ScopePer minutePer hour
Reads and ordinary writes1202000
Status polling and validation1202000
Render, estimate and album orders10200

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.

StatusCodeMeaningRetryable
401The key is missing, revoked or expired.No
402AGENT_CREDITS_EXHAUSTEDThe account has no api-scope credits left.No
402AGENT_KEY_CAP_REACHEDThis key has spent its cap. Issue another key or raise the cap.No
403This 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
403AGENT_PURCHASE_NOT_ALLOWEDThis key was issued without permission to buy tokens.No
403AGENT_PURCHASE_EXCEEDS_CAPThe purchase would take the key past its spend cap.No
409IDEMPOTENCY_IN_PROGRESSThe first call carrying this Idempotency-Key has not answered yet. Wait and send the same call again.Yes
422The request was understood and refused: a duplicate building name, a rejected photo, an album ordered before the main render finished.No
422IDEMPOTENCY_KEY_REQUIREDA paid call from an API key with no Idempotency-Key header. Nothing was queued; send it again with one.No
422IDEMPOTENCY_KEY_REUSEDThis Idempotency-Key was used for a different request. Use a new value for a new order.No
429The 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 answer says charged when the saved payment method covered it, or requires_human with a checkout link a person opens.
  • The api scope 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 api wallet 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, 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

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

brand_selections

TokenMeaning
siding:brand:12Any product of that manufacturer in that category.
siding:line:40@double-4-dutchlapOne line, on one geometry.
siding:product:88@double-4-dutchlapOne product, fully specified.
paint:brand:3Any colour of that paint brand.
paint:product:412One 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.
MCP session
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