You can generate Visi PDF exports through the Core API and download them into your own systems, for example to archive inspection records or send reports to a client automatically.
Exports are generated in the background. You request an export, check its progress, then download the file once it’s complete.
Before you start
Write scope: requesting an export needs an access token with the
writescope. Create OAuth credentials with the Read and write scope and request your token withscope=read write. A read-only token is refused with a 403, but it can still check an export’s progress. See API credentials.Rate limits: your company can request up to 10 exports per minute, and make up to 100 Core API requests per minute in total. These limits are shared by all of your company’s credentials. Checking progress counts only towards the 100.
Region: the examples below use the AU base URL. If your company is on the EU or US region, use the base URL listed in the Core API overview.
How it works
Request an export with
POST /api/core/v1/exports/visis. You get an export ID back straight away.Check its progress with
GET /api/core/v1/exports/{id}until the status iscompleted.Download the file listed in the completed export.
Step 1: Request an export
Choose the Visis to export in one of two ways: list them by ID with visiIds, or describe them with filters, the same filters as the Visi list in the Visibuild app. Send exactly one of the two.
Exporting specific Visis
POST https://app.apac.visibuild.com/api/core/v1/exports/visis
Authorization: Bearer {access_token}
Content-Type: application/json
{
"projectId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"layout": "summary",
"visiIds": [
"b2c3d4e5-f6a7-8901-bcde-f12345678901",
"c3d4e5f6-a7b8-9012-cdef-123456789012"
]
}
Exporting Visis that match filters
This exports every open or in-progress defect on one level of the building, and the locations inside it:
{
"projectId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"layout": "list",
"filters": {
"statuses": ["open", "in_progress"],
"categories": ["defect"],
"locationIds": ["f6a7b8c9-d0e1-2345-f012-456789012345"]
}
}Request fields
Field | Type | Required | Description |
| string | Yes | The project to export from. |
| string | Yes | The PDF layout: |
| array of strings | One of | The Visis to export. If any ID doesn’t exist or isn’t visible to your company, the whole request is refused and nothing is exported. |
| object | One of | Exports the project’s Visis that match every filter. See Filters below. |
| object | No | What the PDF includes. See Settings below. |
Layouts
Layout | What you get |
| One PDF per Visi, with its nested Visis and attachments. Exporting more than one Visi gives you a zip of PDFs. |
| One PDF with a summary card for each Visi, grouped by location. |
| One PDF with a row for each Visi. |
Every layout can include up to 5,000 Visis.
Filters
Filters work as they do on the Visi list in the Visibuild app, and only find Visis your company can see:
A Visi must match every filter you send. Within a filter that takes a list, matching any one value is enough.
Only top-level Visis are exported, as in the app. Send
"includeNestedVisis": trueto include nested Visis too.Archived Visis are left out. Send
"archived": trueto export archived Visis instead.Send
"filters": {}to export every top-level, unarchived Visi in the project.A filter the API doesn’t know, or a value outside a filter’s list, is refused with a 400 and nothing is exported.
IDs are the ones the Core API returns for the project’s users, companies, teams, locations, tags and so on. Company filters take project company IDs, not company IDs.
Status and type
Filter | Description |
| Any of |
| Any of |
| Narrows the |
| Narrows the |
| IDs of custom Visi types. |
| Categories whose Visis have no custom type. |
|
|
|
|
|
|
|
|
People and companies
Filter | Description |
| Assignees as a list of |
| Project companies assigned, directly or through one of their users or teams. |
| Project teams assigned. |
| One assignee. |
| Reviewers, in the same shape as |
| Project companies reviewing, directly or through one of their users or teams. |
| Project teams reviewing. |
|
|
| Project users who created the Visis. |
| Project companies whose users created the Visis. |
| Project users who closed the Visis. |
| Project companies whose users closed the Visis. |
| Project companies added as viewers. |
Where and when
Filter | Description |
| Locations, each with the locations inside it. |
| Date ranges as a list of |
| The same, for when the Visis were started, created or closed. |
| Any of |
| Only Visis updated after this time, in ISO 8601 format, e.g. |
Other
Filter | Description |
| Text in the Visi’s title or reference. |
| Tags. |
| Defect rounds. |
| Project templates the Visis were created from. |
|
|
|
|
Settings
Setting | Default | Description |
|
| Links to Visis, tickets and URLs. |
|
| The activity table. |
|
| Comments in the activity. |
|
| A folder of PDFs per assignee company, delivered as a zip. |
|
| Adds the original attachment files, in a folder per Visi. Always delivered as a zip. |
Response
The API replies with 202 Accepted and the new export. The Location header gives the URL to check its progress.
HTTP/1.1 202 Accepted
Location: /api/core/v1/exports/d4e5f6a7-b8c9-0123-def0-234567890123
{
"data": {
"export": {
"id": "d4e5f6a7-b8c9-0123-def0-234567890123",
"kind": "visi_pdf",
"layout": "summary",
"status": "pending",
"progress": { "completed": 0, "total": null },
"failureReason": null,
"projectIds": ["a1b2c3d4-e5f6-7890-abcd-ef1234567890"],
"visiCount": 2,
"files": [],
"createdAt": "2026-11-05T22:25:07Z",
"completedAt": null,
"expiresAt": null
}
}
}
Save the id. You’ll need it in Step 2. When you use filters, visiCount tells you how many Visis matched.
Safe to retry: sending the same request again while the export is still pending or in_progress returns that same export rather than starting a new one, even from other credentials in your company.
Step 2: Check progress
GET https://app.apac.visibuild.com/api/core/v1/exports/{id}
Authorization: Bearer {access_token}The response has the same shape as in Step 1. Check every few seconds until the status is completed, failed or cancelled. In our testing, an export of 3 Visis took about 10 seconds and an export of 1,711 Visis took about 40 seconds.
Larger exports are generated in parts, and progress shows how many parts are done, e.g. { "completed": 2, "total": 4 }. total is null until the export has been planned.
You can only read exports requested through the API. Exports people make in the Visibuild app don’t appear here.
Statuses
Status | Meaning |
| Requested but not started yet. |
| Being generated. |
| Finished. The generated file is listed in |
| Generation failed. |
| The export was cancelled and won’t produce a file. |
Export fields
Field | Description |
| The export’s ID. |
| What the export produces. Currently always |
| The layout you requested. |
| See Statuses above. |
| Parts generated so far ( |
| Why the export failed. |
| The projects the export covers. |
| How many Visis the export includes. |
| The generated file. Empty until the export is completed, and after it expires. |
| When the export was requested. |
| When the export finished. |
| After this time the file can no longer be downloaded: 14 days after the export completes. |
Step 3: Download the file
Once the status is completed, files lists the generated file:
"files": [
{
"url": "https://...",
"filename": "Richmond Quarter.pdf",
"contentType": "application/pdf",
"byteSize": 482113
}
]
Field | Description |
| Download link, valid for five minutes. |
| Name of the file. See PDF or zip below. |
|
|
| Size of the file in bytes. |
Download the file with a plain GET on url. The link is already signed, so don’t send your Authorization header with it. The download is refused if you do.
Important: the link expires five minutes after you read the export. If it has expired, call GET /exports/{id} again: every call returns a new link. The file stays available for 14 days, until expiresAt.
PDF or zip
files always holds one file. It’s a PDF when the export comes to a single PDF, and a zip of PDFs otherwise:
You get | When | Named |
A PDF | A |
|
A zip | A | The project’s name, e.g. |
Characters that aren’t allowed in filenames are replaced, and long names are shortened.
Example: request, wait and download
Python
import time
import requests
BASE = "https://app.apac.visibuild.com/api/core/v1"
headers = {"Authorization": f"Bearer {access_token}"}
# 1. Request the export
resp = requests.post(f"{BASE}/exports/visis", headers=headers, json={
"projectId": project_id,
"layout": "summary",
"visiIds": visi_ids,
})
resp.raise_for_status()
export = resp.json()["data"]["export"]
# 2. Wait for it to finish
while export["status"] in ("pending", "in_progress"):
time.sleep(5)
resp = requests.get(f"{BASE}/exports/{export['id']}", headers=headers)
resp.raise_for_status()
export = resp.json()["data"]["export"]
if export["status"] != "completed":
raise RuntimeError(export["failureReason"] or export["status"])
# 3. Download the file (no Authorization header)
for f in export["files"]:
download = requests.get(f["url"])
download.raise_for_status()
with open(f["filename"], "wb") as out:
out.write(download.content)
Node.js
const fs = require('fs');
const BASE = 'https://app.apac.visibuild.com/api/core/v1';
const headers = { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' };
// 1. Request the export
let res = await fetch(`${BASE}/exports/visis`, {
method: 'POST',
headers,
body: JSON.stringify({ projectId, layout: 'summary', visiIds }),
});
if (!res.ok) throw new Error(await res.text());
let exp = (await res.json()).data.export;
// 2. Wait for it to finish
while (exp.status === 'pending' || exp.status === 'in_progress') {
await new Promise((resolve) => setTimeout(resolve, 5000));
res = await fetch(`${BASE}/exports/${exp.id}`, { headers });
if (!res.ok) throw new Error(await res.text());
exp = (await res.json()).data.export;
}
if (exp.status !== 'completed') throw new Error(exp.failureReason || exp.status);
// 3. Download the file (no Authorization header)
for (const file of exp.files) {
const download = await fetch(file.url);
if (!download.ok) throw new Error(`Download failed: ${download.status}`);
fs.writeFileSync(file.filename, Buffer.from(await download.arrayBuffer()));
}Rate limit headers
Every response to POST /exports/visis shows where you are against the limit of 10 export requests per minute:
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 8
X-RateLimit-Reset: 59
X-RateLimit-Reset is the number of seconds until the limit resets. The one-minute window starts at your company’s first request. Requests that are refused, for example with a validation error, still count.
Over the limit, the API replies with 429 Too Many Requests and a Retry-After header giving the seconds to wait before trying again.
Errors
Errors return a JSON body with a message:
{ "error": "No Visis match this request" }Status | Error | What to do |
400 |
| Send either |
400 |
| Add |
400 |
| Include |
400 |
| Check the filter’s name against Filters above. Names are case-sensitive. |
400 |
| Use only the values listed for that filter in Filters above. |
400 |
| Send |
401 |
| Add the |
403 |
| Request your token with |
403 |
| Contact Visibuild support to turn on exports for your company. |
404 |
| Check |
404 |
| One of the |
404 |
| Check the export ID you’re polling. Exports made in the Visibuild app can’t be read through the API. |
422 |
| Narrow your filters, or split the export into several requests. |
422 |
| Your filters matched no Visis. Loosen them, or check you’re not leaving out the nested or archived Visis you want. |
429 |
| Wait for the number of seconds in the |
For the full endpoint reference, see the Core API documentation.
