Skip to main content

floorplan-api

Official Python client for the Floor Plan API: upload a floor plan, get back a binary wall-segmentation PNG mask.

  • Image in, image out. PNG, JPEG, WEBP, or one page of a PDF. The mask comes back as PNG bytes at the input's resolution: 255 = wall, 0 = everything else.
  • Two clients, one surface. Client (synchronous, on requests) and AsyncClient (asyncio, on httpx).
  • Retries that don't double-bill. Transient failures are retried with backoff; a job the server already queued is never resubmitted.
  • Works with paths, raw bytes, or binary file-like objects. Large files go straight to object storage via a presigned URL.
  • Self-hosted friendly: point base_url at any Floor Plan API deployment.

Install

pip install floorplan-api              # sync client (requests + pypdf)
pip install "floorplan-api[async]"     # adds AsyncClient (httpx)

Python 3.9+.

Quickstart

from floorplan_api import Client

client = Client(api_key="fp_test_...")            # or set FLOORPLAN_API_KEY
mask = client.extract("plans/floor1.png")         # PNG, JPEG or WEBP
mask = client.extract("plans/set.pdf", page=3)    # PDF: pick the page

with open("walls.png", "wb") as fh:
    fh.write(mask)

print(mask.width, mask.height, mask.job_id)

extract() returns MaskBytes, a bytes subclass. Write it, hash it, hand it to Pillow or OpenCV as usual; the extra attributes width, height, job_id, request_id and mode (live/test) come from the response headers. For a PDF, page_size_pt and pdf_scale are attached too (see below). The client never post-processes the mask.

Any of these inputs work:

client.extract("plans/floor1.png")          # path string
client.extract(Path("plans/floor1.pdf"))    # pathlib.Path
client.extract(image_bytes)                 # raw bytes (format is sniffed)
client.extract(open("plan.jpg", "rb"))      # binary file-like

The format is detected from the file's leading bytes (PNG, JPEG, WEBP, PDF signatures), falling back to the extension. Anything else raises InvalidRequestError before a request is made.

How your file is sent

Be aware that images and PDFs are handled differently on the way out:

Input What is uploaded
PNG, JPEG, WEBP The file, byte for byte. The client never decodes, resizes or re-encodes an image.
PDF A new single-page PDF containing only the requested page. Built locally with pypdf: the page object is copied with its content stream, resources (fonts, embedded images) and annotations; nothing is rasterised client-side. The other pages, document metadata, bookmarks, attachments and form definitions are not sent.
upload_key Nothing; the object is already in storage. For a multi-page PDF you stored yourself, page= is sent as a form field and the server picks the page.

So a 40-page drawing set costs one page of bandwidth and storage, and the server only ever holds the page you asked about. If you need the whole document on the server side, upload it with upload() from a tool that does not slice, then call extract(upload_key=..., page=N).

PDFs and page

A PDF is processed one page at a time. Pass page= (1-based; default 1) to say which. Page errors (out of range, password-protected, unreadable) are raised as InvalidRequestError before anything is sent. The API rasterises the page at 200 DPI (longest edge capped at 8192 px) and returns the mask at that size; read it from mask.width and mask.height.

mask = client.extract("set.pdf", page=3)
key = client.upload("set.pdf", page=3)        # the stored object is page 3 only
mask = client.extract(upload_key=key)         # ... so no page is needed here

page on a raster input is rejected unless it is 1. When you submit by upload_key for an object you stored yourself (raw REST), page= is sent to the server, which renders that page of the stored file.

Mapping the mask back to PDF coordinates. The mask is on the rendered page's pixel grid, not in PDF points. The client measures the page's crop box (honouring /Rotate) before upload and attaches it, so:

mask = client.extract("set.pdf", page=3)
mask.page_size_pt      # (1728.0, 2592.0)  -> a 24 x 36 in sheet
mask.pdf_scale         # mask pixels per PDF point: mask.width / page width
mask.pdf_dpi           # the DPI actually used: 200, or less if the page hit the 8192 px cap

x_px = x_pt * mask.pdf_scale   # PDF point -> mask pixel (origin: top-left of the render)

floorplan_api.pdf_page_size(data, page) gives the same (width, height) in points for any PDF, for example to compute the scale for a mask you fetched later with download_mask(), which has no page_size_pt.

Async

import asyncio
from floorplan_api import AsyncClient

async def main() -> None:
    async with AsyncClient() as client:
        masks = await asyncio.gather(
            client.extract("a.pdf"),
            client.extract("b.png"),
        )
        for m in masks:
            print(m.size)

asyncio.run(main())

AsyncClient has the same methods as Client, all awaitable. Pass your own httpx.AsyncClient as client= for proxies or HTTP/2; the wrapper then leaves it open.

Large files

Inline uploads are capped at 10 MB. upload_then_extract() switches to a presigned upload for anything bigger, so the API server never holds the bytes:

mask = client.upload_then_extract("big_floor_plan.pdf", page=2)

# or step by step:
key = client.upload("big_floor_plan.pdf", page=2)   # PUT straight to object storage
mask = client.extract(upload_key=key)               # submit by storage key

The size check happens after the page is cut out, so a large multi-page PDF whose selected page is small still takes the inline path.

analyze() and analyze_async() accept upload_key= the same way.

What the API does with your file

Nothing on the client or the API server touches pixels; the worker does, like this (the full trace is in docs/IMAGE_PIPELINE.md of the API repo):

  • Rasters are decoded with OpenCV in colour mode. Alpha is dropped without compositing, so flatten transparent PNGs onto white first; grayscale is expanded to three channels; 16-bit depth becomes 8-bit; JPEG EXIF orientation is applied, so the mask aligns with the displayed orientation; ICC profiles are ignored. A raster whose longer edge exceeds 8192 px is processed downscaled to that bound and the mask is resized back to the input size, so it stays pixel-aligned but carries less detail.
  • PDF pages are rendered at 200 DPI onto white (transparent regions composite onto white), reduced so the longer edge is at most 8192 px. All content is rendered: linework, hatching, text, dimensions. The mask has the rendered size, reported in width/height.
  • Inference is a two-stage U-Net++ (whole sheet at shortest side 1024, then a crop refiner at native resolution). No test-time augmentation.
  • Output is prob > 0.5 as an 8-bit single-channel PNG with values exactly 0 and 255, no morphology or filtering. 255 is wall in the carved convention: door and window openings are not wall, and walls are as thick as the source linework.

Limits you will meet: inline uploads 10 MB; presigned URLs valid 15 min; beta and Free keys 10 requests per minute; the sync endpoints wait 30 s for the worker before answering 504 with the job id; the queue answers 503 with Retry-After: 30 when 100 jobs are pending. extract costs 1 credit, analyze 2, only on live keys.

Authentication

API keys come from the Floor Plan API dashboard.

  • Live keys (fp_live_...) — production use, billed against your account.
  • Test keys (fp_test_...) — same model, never billed.
client = Client(api_key="fp_live_xxx")
# or, equivalently:
import os; os.environ["FLOORPLAN_API_KEY"] = "fp_live_xxx"
client = Client()

Background jobs

For batches, submit a job and collect the mask later:

job = client.analyze_async("plan.png")
print(f"Submitted {job.id}, status={job.status}")

final = client.wait_for_job(job.id, poll_interval=2.0, timeout=300.0)
if final.status == "completed":
    mask = client.download_mask(final.id)
    print(f"got {final.result.width}x{final.result.height} mask")

# Or poll yourself:
job = client.get_job(job.id)
if job.is_terminal:
    ...

analyze and extract currently produce identical output; the two endpoints are kept distinct so future tiers can attach to analyze without breaking extract's simpler contract.

Timeouts on a busy queue

The synchronous endpoints wait about 30 s for the worker. If the queue is deep the server answers 504 and includes the job's id; the job keeps running. The client raises TimeoutError with job_id set and does not retry (a retry would queue a second copy). Finish the job without resubmitting:

from floorplan_api import TimeoutError

try:
    mask = client.extract("plan.pdf")
except TimeoutError as exc:
    if exc.job_id is None:
        raise                                   # client-side timeout
    job = client.wait_for_job(exc.job_id)
    mask = client.download_mask(job.id)

Errors

All errors derive from FloorPlanError. Catch the base class to handle every API error, or specific subclasses to take action:

from floorplan_api import (
    Client, FloorPlanError,
    AuthenticationError, RateLimitError, InvalidRequestError, NotFoundError,
    ServerError, ProcessingError, TimeoutError, ConnectionError,
)

try:
    mask = client.extract("plan.png")
except RateLimitError as exc:
    time.sleep(exc.retry_after or 5.0)
except ProcessingError as exc:
    print(f"worker could not process this file: {exc.message} (job {exc.job_id})")
except AuthenticationError:
    print("Check your API key.")
except FloorPlanError as exc:
    print(f"{exc.type}: {exc.message} (request_id={exc.request_id})")
Exception Status When Retried
AuthenticationError 401, 403 Missing/invalid/expired/revoked key; job belongs to another account no
InvalidRequestError 400, 409, 413, 415 Malformed body, bad page, mask requested before completion, file too large, unsupported type. Also raised locally for unsupported input or a PDF page that does not exist no
NotFoundError 404 Job/resource missing no
RateLimitError 429 Per-minute rate limit exceeded yes, honouring Retry-After
TimeoutError 504 Worker did not finish in the sync window; job_id set no
ProcessingError 500 Worker failed the job (undecodable file, ...); job_id set no
ServerError other 5xx Outage, queue at capacity (503 honours Retry-After) yes
TimeoutError — Client-side timeout exceeded, or wait_for_job gave up connection timeouts yes
ConnectionError — DNS / TCP / TLS failure yes

Every exception carries status_code, type, request_id, job_id and the decoded response body when available.

Configuration

client = Client(
    api_key="fp_live_...",
    base_url="https://api.floorplanapi.com",   # default
    timeout=60.0,                              # seconds per request
    max_retries=3,                             # connection errors, 429, transient 5xx
    retry_backoff=0.5,                         # base delay (s) for exp backoff w/ jitter
    session=None,                              # your own requests.Session
)

AsyncClient takes the same arguments, with client= (an httpx.AsyncClient) in place of session=.

A server Retry-After header overrides the backoff, capped at 30 s per attempt. With the defaults, a 503 "queue at capacity" response can hold an extract() call for up to about 90 s before it raises.

Environment variables:

  • FLOORPLAN_API_KEY — used when api_key= is omitted.
  • FLOORPLAN_BASE_URL — used when base_url= is omitted (handy for self-hosted).

Pointing at a self-hosted instance

client = Client(
    api_key="fp_test_...",
    base_url="http://localhost:3000",
)

The Next.js app rewrites /v1/* to /api/v1/* internally, so the client's base URL is the bare host with no /api segment.

Examples

See examples/:

Development

pip install -e '.[dev]'
pytest
ruff check src tests examples
mypy src

Releases: bump src/floorplan_api/_version.py, add a changelog entry, and push a python-v<version> tag. CI runs the tests on Python 3.9–3.13, builds the sdist and wheel, and publishes to PyPI via trusted publishing.

License

MIT — see LICENSE.

Release files for floorplan-api 0.5.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for floorplan-api 0.5.0
File Size Uploaded
floorplan_api-0.5.0.tar.gz 36.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for floorplan-api 0.5.0
File Interpreter ABI Platform
floorplan_api-0.5.0-py3-none-any.whl Python 3 none any Details

Total release size: 66.0 kB

Release files / floorplan_api-0.5.0.tar.gz

Download URL floorplan_api-0.5.0.tar.gz
Size 36.3 kB
Tags Source
SHA-256 checksum
How to use checksums
d14fdbfbb5a77f0ff1b1fe1c5aae4d5ccd923a0428200c5a2c7f7615cde77fc6
BLAKE2b-256 checksum
How to use checksums
9b33df29244e12bf876bd06e1007a94df6caf40573885df0afbdbb120bc626e3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.

Transparency log

Release files / floorplan_api-0.5.0-py3-none-any.whl

Download URL floorplan_api-0.5.0-py3-none-any.whl
Size 29.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
9e14caf8ba6ba049bff2d3f09cda3436313e68d78027387f7ae1de76bfe40a50
BLAKE2b-256 checksum
How to use checksums
b6b01406617d3ae0415de0ff94080924c9c42ba032495dd00f022e8235712978
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.5.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page