Developers

FunBIM API

Bring your site data into your own tools: BI, ERP, data warehouse, scripts. A documented, stable, read-only REST API.

v1.0.0 · https://api.funbim.com/v1

Overview

The FunBIM API gives read-only access to the data of all your company's projects:

  • projects, their configuration (trades, statuses, types, priorities, companies) and spatial structure;
  • members and their permissions;
  • issues and forms, with their answers, comments, sub-tasks and signatories;
  • document management files, coded documents and their approval workflow;
  • photos, progress records and equipment;
  • a full export of a project as a ZIP archive.

The API is included in the FunBIM 3D company subscription. Keys created in FunBIM are read-only: an integration cannot alter your projects.

Quick start

1. Create a key in FunBIM. You need to be an administrator of the company, on a FunBIM 3D subscription:

  • in My Projects, open your organization's card ("Organization settings, applied to all your projects");
  • in the left-hand menu, choose API;
  • click Create a key, give it a name (for example "Power BI") and a validity period.

The key is shown only once: copy it straight away and keep it like a password. The same screen lists your keys and their last use, and lets you revoke one.

2. List your company's projects:

curl https://api.funbim.com/v1/projects \
  -H "Authorization: Bearer fbk_live_XXXXXXXXXXXX_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"

3. You get the list of your projects:

{
  "data": [
    {
      "id": "-NLKzINUsK6CcZspR6B6",
      "name": "Résidence Les Tilleuls",
      "customId": "2026-014",
      "status": "active",
      "address": "12 rue des Tilleuls, 69003 Lyon",
      "createdAt": "2026-01-09T10:04:37.722Z",
      "updatedAt": "2026-10-14T16:52:35.162Z",
      "createdBy": { "visId": "-MUiZYD9AXG_hj7GjYyr", "name": "Claire Martin" },
      "spatialLabels": { "buildings": "Bâtiments" }
    }
  ],
  "nextCursor": null
}

Authentication

Every request carries the key in the Authorization: Bearer fbk_live_… header. A key belongs to the company, not to a person: it gives access to all the company's projects, and to them only.

  • A company can have up to 10 active keys, one per use (BI, ERP…).
  • Each key has a validity period (1 year at most) and can be revoked at any time; revocation takes effect within a minute.
  • Use the key server-side only. Never embed it in a web page or a mobile app.

A missing, invalid, revoked or expired key always gets the same 401 invalid_key response, whatever the cause.

Security and isolation

The API is built so that a key can never read anything but its own company's data.

  • Isolation per company: every request is tied to the key's company, and the scope is checked before any read. A project of another company answers 404, exactly like a missing project: you cannot even tell it exists.
  • Read-only: a key created in FunBIM never changes your data. Starting an export is the only accepted action.
  • Protected keys: FunBIM never stores your key in clear text, only a fingerprint. It is shown once, when created; if you lose it, revoke it and create another one.
  • Files: binaries are only served through short-lived signed links (15 minutes, 24 hours for an export archive), and only for files of the requested project.
  • Minimal personal data: people are given by a public identifier and their name, never by their email or login identifier.
  • Plain errors: error responses carry no internal technical detail.

Good practices:

  • one key per tool (BI, ERP…), so you can revoke one without cutting the others;
  • keep it in a secrets manager, never in source code or a shared spreadsheet;
  • a key sees every project of the company: keep it for trusted tools.

These guarantees are checked by automated tests, including cross-company access attempts run against the deployed API.

Conventions

  • Responses are UTF-8 JSON; a resource is in data, a list in data + nextCursor.
  • Dates are ISO 8601 UTC (2026-10-14T08:30:00.000Z); a missing value is null.
  • People are given as { visId, name }, configuration items (status, type, trade…) as { id, label }. The full lists are in /projects/{projectId}/config.
  • name is null when a person's name is not set, or is only an email address: the API never publishes emails. The visId still identifies the person.
  • Form answers are in formData, a label → value object ready to use.
  • Deleted items are excluded, unless you pass includeDeleted=true.
  • New fields may appear: ignore the ones you don't know.

Pagination and sync

Lists use cursor pagination: pass nextCursor back as the cursor parameter until you get null. limit defaults to 100, 500 at most.

A page may hold fewer items than limit, or none at all, without being the last one. Only nextCursor: null marks the end: always loop on the cursor, never on the page size.

JavaScript (Node 18+)
const BASE = "https://api.funbim.com/v1";
const headers = { Authorization: `Bearer ${process.env.FUNBIM_API_KEY}` };

async function listAll(path) {
  const items = [];
  let cursor = null;
  do {
    const url = new URL(BASE + path);
    url.searchParams.set("limit", "500");
    if (cursor) url.searchParams.set("cursor", cursor);
    const res = await fetch(url, { headers });
    if (res.status === 429) {
      await new Promise((r) => setTimeout(r, Number(res.headers.get("Retry-After") ?? 1) * 1000));
      continue;
    }
    const body = await res.json();
    items.push(...body.data);
    cursor = body.nextCursor; // null = end of the list
  } while (cursor);
  return items;
}

const observations = await listAll("/projects/PROJECT_ID/observations");
Python
import os, time, requests

BASE = "https://api.funbim.com/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['FUNBIM_API_KEY']}"}

def list_all(path, **params):
    items, cursor = [], None
    while True:
        query = {"limit": 500, **params, **({"cursor": cursor} if cursor else {})}
        res = requests.get(BASE + path, headers=HEADERS, params=query)
        if res.status_code == 429:
            time.sleep(int(res.headers.get("Retry-After", 1)))
            continue
        res.raise_for_status()
        body = res.json()
        items += body["data"]
        cursor = body["nextCursor"]
        if cursor is None:
            return items

forms = list_all("/projects/PROJECT_ID/observations", kind="form")

For an incremental sync, only ask for what changed since your last run, deletions included:

GET /v1/projects/PROJECT_ID/observations?updatedSince=2026-10-01T00:00:00Z&includeDeleted=true

Location on drawings and models

An issue or a form can be located in three ways, which can be combined: on a PDF drawing (planLocation), in the model (modelLocation) and in the project structure (spatialLocation). A field is null when the item isn't located that way. fileId gives the drawing or model concerned.

On a PDF drawing — the position is given on the page as displayed, rotation included, from the top-left corner:

"planLocation": {
  "fileId": "-NLLAA7v50nyjO5a2FEm",
  "page": 1,
  "x": 0.521,
  "y": 0.512,
  "rotation": 270,
  "xPt": 1350.43,
  "yPt": 884.62,
  "pageWidthPt": 2592,
  "pageHeightPt": 1728
}
  • x, y: position as a fraction of the page (0 to 1). To place the pin on an image of the drawing: x × image width, y × image height.
  • xPt, yPt: the same position in PDF points; pageWidthPt, pageHeightPt: the size of the displayed page, in PDF points.
  • page starts at 1; rotation is the page rotation (0, 90, 180 or 270).
  • The position stays right from one revision of the drawing to the next, even if the page is rotated differently.

In the model — kind tells which kind of point it is:

"modelLocation": {
  "kind": "3d",
  "fileId": "-O7_mttUaADaWQEW9EMM",
  "viewableGuid": "fe9402fe-49f5-4c05-9873-8601750db635-001e911a",
  "sheetName": null,
  "linkedFileId": null,
  "elementGuid": "ad3aac00-f6ca-4a67-9ecb-94800e6c7479-0013a9a2",
  "x": 94.99,
  "y": -79.56,
  "z": 20.53
}
  • kind: "3d": a point in the model; x, y, z are model coordinates, elementGuid is the element pointed at.
  • kind: "sheet": a point on a 2D sheet of the model, named by sheetName; x, y are coordinates on that sheet and z is 0.
  • viewableGuid is the model view where the point was placed; linkedFileId the linked model, for a federated model.

In the project structure — building, level, area and unit, with their label:

"spatialLocation": {
  "buildingId": "-NQUhGd5NcOGBgfj8u8N",
  "levelId": null,
  "areaId": null,
  "housingId": null,
  "building": { "id": "-NQUhGd5NcOGBgfj8u8N", "label": "Building A" },
  "level": null,
  "area": null,
  "housing": null
}

The plain identifiers (buildingId…) are still there. The full structure is in /projects/{projectId}/spatial.

Files and photos

Binaries are never in JSON responses. Each file and photo carries a downloadPath: call it with your key and the API answers 302 to a signed download URL, valid for 15 minutes.

curl -L -o drawing.pdf \
  -H "Authorization: Bearer $FUNBIM_API_KEY" \
  https://api.funbim.com/v1/projects/PROJECT_ID/files/FILE_ID/download

For a photo, ?size=thumb returns the thumbnail when there is one.

ZIP export of a project

To archive a whole project or migrate your data, start an export: FunBIM builds a ZIP archive with all the data in the API format, an Excel sheet of issues and forms, and, if requested, the files, photos and signatures.

1. Start the export
curl -X POST https://api.funbim.com/v1/projects/PROJECT_ID/exports \
  -H "Authorization: Bearer $FUNBIM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "includeMedia": true }'

# 202 Accepted
{ "data": { "id": "EXPORT_ID", "status": "pending", … } }
2. Poll the export, then download
curl https://api.funbim.com/v1/projects/PROJECT_ID/exports/EXPORT_ID \
  -H "Authorization: Bearer $FUNBIM_API_KEY"

# once "status": "done"
{ "data": { "status": "done", "sizeBytes": 371160353,
            "downloadUrl": "https://storage.googleapis.com/…", … } }
  • The archive is ready within minutes; poll the export every 10 to 30 seconds.
  • The downloadUrl link is valid for 24 hours and can be requested again; the archive is kept for 7 days.
  • Above 5 GB of files, the archive is built without them: download them afterwards through their downloadPath.
  • One export at a time per project, 10 exports per day per company.

Limits

LimitValue
Rate120 requests per minute per key
Quota50,000 requests per day per company (reset at midnight UTC)
Exports1 running per project, 10 per day per company

Beyond that, the API answers 429 with a Retry-After header giving the wait in seconds.

Errors

An error always returns an HTTP status and an error object:

HTTP/2 404
{ "error": { "code": "not_found", "message": "Resource not found." } }
HTTPCodeMeaning
400invalid_requestInvalid or repeated parameter
401invalid_keyMissing, invalid, revoked or expired key
403plan_requiredThe company's subscription doesn't include the API
404not_foundResource missing or outside your company
405invalid_requestMethod not allowed (the API is read-only)
409export_in_progressAn export of this project is already running
429rate_limited / quota_exceededRate or daily quota exceeded
500internalInternal error, try again

Versioning

Version 1 (/v1) only receives additions: new fields, new endpoints. A breaking change will go through a version 2, announced at least 12 months ahead.

Reference

Every version 1 endpoint, generated from the API contract. Paths are relative to the base URL https://api.funbim.com/v1.

Projects

GET/projects→ 200

Your company's projects

Response
FieldType
idstring
namestring
customIdstring | null
status"active" | "archived"
addressstring | null
createdAtdate-time
updatedAtdate-time
createdByobject
createdBy.visIdstring
createdBy.namestring | null
spatialLabelsobject
spatialLabels.buildings · optionalstring
spatialLabels.levels · optionalstring
spatialLabels.areas · optionalstring
spatialLabels.housings · optionalstring

GET/projects/{projectId}→ 200

Project details

Parameters
FieldType
projectId · pathstring
Response
FieldType
idstring
namestring
customIdstring | null
status"active" | "archived"
addressstring | null
createdAtdate-time
updatedAtdate-time
createdByobject
createdBy.visIdstring
createdBy.namestring | null
spatialLabelsobject
spatialLabels.buildings · optionalstring
spatialLabels.levels · optionalstring
spatialLabels.areas · optionalstring
spatialLabels.housings · optionalstring

GET/projects/{projectId}/config→ 200

Trades, statuses, types, priorities, roles, fields, companies

Parameters
FieldType
projectId · pathstring
Response
FieldType
tradesobject[]
statusesobject[]
typesobject[]
prioritiesobject[]
rolesobject[]
fieldsobject[]
companiesobject[]

GET/projects/{projectId}/spatial→ 200

Buildings, levels, areas, housing units

Parameters
FieldType
projectId · pathstring
Response
FieldType
buildingsobject[]
levelsobject[]
areasobject[]
housingsobject[]

GET/projects/{projectId}/members→ 200

Project members

Parameters
FieldType
projectId · pathstring
Response
FieldType
visIdstring
namestring | null
companystring | null
role"owner" | "admin" | "member" | "viewer" | "custom"
tradeIdsstring[]
permissionsobject
permissions.create"all" | "ownTrades" | "none"
permissions.read"all" | "ownTrades"
permissions.edit"all" | "ownTrades" | "ownItemsInOwnTrades" | "commentsOnly"
permissions.close"all" | "ownTrades" | "none"
permissions.delete"all" | "ownItems" | "none"
permissions.canImportboolean
permissions.canConfigureboolean
joinedAtdate-time | null

Issues and forms

GET/projects/{projectId}/observations→ 200

Issues and forms

Parameters
FieldType
projectId · pathstring
kind · query, optional"observation" | "form"
cursor · query, optionalstring
limit · query, optionalinteger
includeDeleted · query, optionalboolean
updatedSince · query, optionaldate-time
Response

Paginated list: data is an array, nextCursor reads the next page.

FieldType
idstring
kind"observation" | "form"
numberinteger | null
customIdstring | null
titlestring
descriptionstring | null
statusobject
status.idstring
status.labelstring | null
status.category"opened" | "intermediate" | "closed"
typeobject | null
type.idstring
type.labelstring | null
priorityobject | null
priority.idstring
priority.labelstring | null
tradesobject[]
allTradesboolean
assignedMembersobject[]
assignedCompaniesobject[]
dueDatedate | null
fileIdstring | null
planLocationobject | null
planLocation.fileIdstring
planLocation.pageinteger
planLocation.xnumber | null
planLocation.ynumber | null
planLocation.rotation"0" | "90" | "180" | "270"
planLocation.xPtnumber | null
planLocation.yPtnumber | null
planLocation.pageWidthPtnumber | null
planLocation.pageHeightPtnumber | null
modelLocationobject | null
modelLocation.kind"3d" | "sheet"
modelLocation.fileIdstring
modelLocation.viewableGuidstring | null
modelLocation.sheetNamestring | null
modelLocation.linkedFileIdstring | null
modelLocation.elementGuidstring | null
modelLocation.xnumber
modelLocation.ynumber
modelLocation.znumber
spatialLocationobject | null
spatialLocation.buildingIdstring | null
spatialLocation.levelIdstring | null
spatialLocation.areaIdstring | null
spatialLocation.housingIdstring | null
spatialLocation.buildingobject | null
spatialLocation.levelobject | null
spatialLocation.areaobject | null
spatialLocation.housingobject | null
createdAtdate-time
createdByobject
createdBy.visIdstring
createdBy.namestring | null
updatedAtdate-time
updatedByobject | null
updatedBy.visIdstring
updatedBy.namestring | null
closedAtdate-time | null
closedByobject | null
closedBy.visIdstring
closedBy.namestring | null
archivedboolean
highlightedboolean
deletedboolean
photoIdsstring[]
formDatamap<string, string>
signaturesobject[]

GET/projects/{projectId}/observations/{observationId}→ 200

Details (sub-tasks and comments included)

Parameters
FieldType
projectId · pathstring
observationId · pathstring
Response
FieldType
idstring
kind"observation" | "form"
numberinteger | null
customIdstring | null
titlestring
descriptionstring | null
statusobject
status.idstring
status.labelstring | null
status.category"opened" | "intermediate" | "closed"
typeobject | null
type.idstring
type.labelstring | null
priorityobject | null
priority.idstring
priority.labelstring | null
tradesobject[]
allTradesboolean
assignedMembersobject[]
assignedCompaniesobject[]
dueDatedate | null
fileIdstring | null
planLocationobject | null
planLocation.fileIdstring
planLocation.pageinteger
planLocation.xnumber | null
planLocation.ynumber | null
planLocation.rotation"0" | "90" | "180" | "270"
planLocation.xPtnumber | null
planLocation.yPtnumber | null
planLocation.pageWidthPtnumber | null
planLocation.pageHeightPtnumber | null
modelLocationobject | null
modelLocation.kind"3d" | "sheet"
modelLocation.fileIdstring
modelLocation.viewableGuidstring | null
modelLocation.sheetNamestring | null
modelLocation.linkedFileIdstring | null
modelLocation.elementGuidstring | null
modelLocation.xnumber
modelLocation.ynumber
modelLocation.znumber
spatialLocationobject | null
spatialLocation.buildingIdstring | null
spatialLocation.levelIdstring | null
spatialLocation.areaIdstring | null
spatialLocation.housingIdstring | null
spatialLocation.buildingobject | null
spatialLocation.levelobject | null
spatialLocation.areaobject | null
spatialLocation.housingobject | null
createdAtdate-time
createdByobject
createdBy.visIdstring
createdBy.namestring | null
updatedAtdate-time
updatedByobject | null
updatedBy.visIdstring
updatedBy.namestring | null
closedAtdate-time | null
closedByobject | null
closedBy.visIdstring
closedBy.namestring | null
archivedboolean
highlightedboolean
deletedboolean
photoIdsstring[]
formDatamap<string, string>
signaturesobject[]
subtaskListsobject[]
commentsobject[]

Documents

GET/projects/{projectId}/files→ 200

Document management files and folders

Parameters
FieldType
projectId · pathstring
cursor · query, optionalstring
limit · query, optionalinteger
includeDeleted · query, optionalboolean
updatedSince · query, optionaldate-time
Response

Paginated list: data is an array, nextCursor reads the next page.

FieldType
idstring
namestring
titlestring | null
kind"folder" | "pdf" | "model3d" | "image" | "other"
mimeTypestring | null
sizeBytesinteger
parentIdstring | null
revisionRootIdstring
ownerobject
owner.visIdstring
owner.namestring | null
createdAtdate-time
updatedAtdate-time
deletedboolean
downloadPathstring | null

GET/projects/{projectId}/files/{fileId}/download→ 302

Download a file

Parameters
FieldType
projectId · pathstring
fileId · pathstring
Response

Redirects to a signed download URL, valid for 15 minutes.

GET/projects/{projectId}/documents→ 200

Coded documents (drawing lists) and approval workflow

Parameters
FieldType
projectId · pathstring
cursor · query, optionalstring
limit · query, optionalinteger
includeDeleted · query, optionalboolean
updatedSince · query, optionaldate-time
Response

Paginated list: data is an array, nextCursor reads the next page.

FieldType
idstring
fileIdstring | null
fileNamestring
titlestring | null
statusobject
status.idstring
status.labelstring | null
codificationmap<string, string>
dueDatedate | null
previousFileIdsstring[]
visaCountinteger
responseCountinteger
transmittalNumbersinteger[]
createdAtdate-time
updatedAtdate-time
deletedboolean

Photos

GET/projects/{projectId}/photos→ 200

Photos

Parameters
FieldType
projectId · pathstring
cursor · query, optionalstring
limit · query, optionalinteger
includeDeleted · query, optionalboolean
updatedSince · query, optionaldate-time
Response

Paginated list: data is an array, nextCursor reads the next page.

FieldType
idstring
attachedToobject
attachedTo.kind"observation" | "form" | "comment" | "other"
attachedTo.idstring | null
ownerobject
owner.visIdstring
owner.namestring | null
contentTypestring | null
sizeBytesinteger
createdAtdate-time
deletedboolean
downloadPathstring
thumbnailPathstring | null

GET/projects/{projectId}/photos/{photoId}/download→ 302

Download a photo

Parameters
FieldType
projectId · pathstring
photoId · pathstring
size · query, optional"full" | "thumb"
Response

Redirects to a signed download URL, valid for 15 minutes.

Progress and equipment

GET/projects/{projectId}/progress→ 200

Progress records

Parameters
FieldType
projectId · pathstring
cursor · query, optionalstring
limit · query, optionalinteger
includeDeleted · query, optionalboolean
updatedSince · query, optionaldate-time
Response

Paginated list: data is an array, nextCursor reads the next page.

FieldType
idstring
typeobject
type.idstring
type.labelstring | null
statusobject | null
status.idstring
status.labelstring | null
fileIdstring
measurenumber
unitstring
authorobject
author.visIdstring
author.namestring | null
createdAtdate-time
updatedAtdate-time | null
deletedboolean
xfdfstring | null

GET/projects/{projectId}/equipments→ 200

Equipment

Parameters
FieldType
projectId · pathstring
cursor · query, optionalstring
limit · query, optionalinteger
includeDeleted · query, optionalboolean
updatedSince · query, optionaldate-time
Response

Paginated list: data is an array, nextCursor reads the next page.

FieldType
idstring
namestring
categorystring | null
customIdstring | null
typeobject | null
type.idstring
type.labelstring | null
statusobject | null
status.idstring
status.labelstring | null
fileIdstring | null
linkedFileIdstring | null
externalIdstring | null
measurenumber | null
measureUnit"m" | "m2" | "unit" | null
authorobject
author.visIdstring
author.namestring | null
createdAtdate-time
updatedAtdate-time
closedAtdate-time | null
closedByobject | null
closedBy.visIdstring
closedBy.namestring | null
stateHistoryobject[]
deletedboolean

ZIP export

POST/projects/{projectId}/exports→ 202

Start a ZIP export of the project

Parameters
FieldType
projectId · pathstring
Request body
FieldType
includeMedia · optionalboolean
Response
FieldType
idstring
projectIdstring
status"pending" | "processing" | "done" | "failed"
includeMediaboolean
createdAtdate-time
finishedAtdate-time | null
sizeBytesinteger | null
downloadUrlstring | null
downloadUrlExpiresAtdate-time | null
expiresAtdate-time | null
errorstring | null

GET/projects/{projectId}/exports/{exportId}→ 200

Export status (and ZIP link once ready)

Parameters
FieldType
projectId · pathstring
exportId · pathstring
Response
FieldType
idstring
projectIdstring
status"pending" | "processing" | "done" | "failed"
includeMediaboolean
createdAtdate-time
finishedAtdate-time | null
sizeBytesinteger | null
downloadUrlstring | null
downloadUrlExpiresAtdate-time | null
expiresAtdate-time | null
errorstring | null

Connect FunBIM to your tools

The API is included in the FunBIM 3D company subscription. Let's talk about your integration.

Contact us