Skip to main content

Exporting Visi PDFs via API

How to request Visi PDF exports through the Core API, track their progress, and download the finished files.

Written by Hugh Evans

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 write scope. Create OAuth credentials with the Read and write scope and request your token with scope=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

  1. Request an export with POST /api/core/v1/exports/visis. You get an export ID back straight away.

  2. Check its progress with GET /api/core/v1/exports/{id} until the status is completed.

  3. 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

projectId

string

Yes

The project to export from.

layout

string

Yes

The PDF layout: full_details, summary or list. See Layouts below.

visiIds

array of strings

One of visiIds or filters

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.

filters

object

One of visiIds or filters

Exports the project’s Visis that match every filter. See Filters below.

settings

object

No

What the PDF includes. See Settings below.

Layouts

Layout

What you get

full_details

One PDF per Visi, with its nested Visis and attachments. Exporting more than one Visi gives you a zip of PDFs.

summary

One PDF with a summary card for each Visi, grouped by location.

list

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": true to include nested Visis too.

  • Archived Visis are left out. Send "archived": true to 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

statuses

Any of open, in_progress, in_review, closed, na, cant_close and in_dispute.

categories

Any of inspection, task, ncr, issue and defect.

taskTypes

Narrows the task category to any of task, hold_point and witness_point. Send it with "categories": ["task"].

issueTypes

Narrows the issue category to any of issue, incorrect_works and incomplete_works. Send it with "categories": ["issue"].

subtypeIds

IDs of custom Visi types.

noSubtypeCategories

Categories whose Visis have no custom type.

holdPoints

skipped for hold points that were skipped, or not_skipped for the rest.

blockingHoldPoints

true for hold points that are blocking other Visis.

blockedByHoldPoints

true for Visis blocked by a hold point.

closedDisputed

true for Visis closed under dispute, false for the rest.

People and companies

Filter

Description

assignees

Assignees as a list of { "id": "…", "type": "ProjectUser" } or { "id": "…", "type": "ProjectCompany" }.

assigneeCompanyIds

Project companies assigned, directly or through one of their users or teams.

assigneeTeamIds

Project teams assigned.

assigneeId and assigneeType

One assignee. assigneeType is ProjectUser, ProjectCompany or ProjectTeam. Send both or neither.

reviewers

Reviewers, in the same shape as assignees.

reviewerCompanyIds

Project companies reviewing, directly or through one of their users or teams.

reviewerTeamIds

Project teams reviewing.

reviewerMatch

is_a_reviewer (the default) matches any reviewer. is_current_reviewer matches only the reviewer whose turn it is.

creatorIds

Project users who created the Visis.

creatorCompanyIds

Project companies whose users created the Visis.

closedByIds

Project users who closed the Visis.

closedByCompanyIds

Project companies whose users closed the Visis.

viewerCompanyIds

Project companies added as viewers.

Where and when

Filter

Description

locationIds

Locations, each with the locations inside it.

dueDateRanges

Date ranges as a list of { "from": "2026-11-01", "to": "2026-11-30" }, in the project’s time zone. Either end can be left out.

startedDateRanges, createdDateRanges, closedDateRanges

The same, for when the Visis were started, created or closed.

daysOpen

Any of zero_to_ten_days, eleven_to_twenty_nine_days, thirty_to_fifty_nine_days, sixty_to_eighty_nine_days, sixty_days_or_more and ninety_days_or_more.

updatedAfter

Only Visis updated after this time, in ISO 8601 format, e.g. "2026-11-01T00:00:00Z".

Other

Filter

Description

titleSearch

Text in the Visi’s title or reference.

tagIds

Tags.

defectRoundIds

Defect rounds.

projectTemplateIds

Project templates the Visis were created from.

includeNestedVisis

true to include nested Visis. Defaults to false.

archived

true to export archived Visis instead. Defaults to false.

Settings

Setting

Default

Description

includeLinks

true

Links to Visis, tickets and URLs. full_details only.

includeActivity

true

The activity table. full_details only.

includeComments

false

Comments in the activity. full_details only.

splitPerCompany

false

A folder of PDFs per assignee company, delivered as a zip.

includeAttachmentFiles

false

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

pending

Requested but not started yet.

in_progress

Being generated. progress shows how far along it is.

completed

Finished. The generated file is listed in files.

failed

Generation failed. failureReason explains why.

cancelled

The export was cancelled and won’t produce a file.

Export fields

Field

Description

id

The export’s ID.

kind

What the export produces. Currently always visi_pdf.

layout

The layout you requested.

status

See Statuses above.

progress

Parts generated so far (completed) out of the parts planned (total).

failureReason

Why the export failed. null otherwise.

projectIds

The projects the export covers.

visiCount

How many Visis the export includes.

files

The generated file. Empty until the export is completed, and after it expires.

createdAt

When the export was requested.

completedAt

When the export finished.

expiresAt

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

url

Download link, valid for five minutes.

filename

Name of the file. See PDF or zip below.

contentType

application/pdf or application/zip.

byteSize

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 summary or list export, or a full_details export of one Visi.

summary and list: the project’s name, e.g. Richmond Quarter.pdf. full_details: the Visi’s location, reference and title, e.g. Level 3 - RQ-1 - Waterproofing.pdf.

A zip

A full_details export of more than one Visi, splitPerCompany or includeAttachmentFiles, or a large export generated in several parts.

The project’s name, e.g. Richmond Quarter.zip. A PDF split into parts is numbered inside the zip, e.g. Richmond Quarter (part 1 of 3).pdf.

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

visiIds, filters are mutually exclusive

Send either visiIds or filters, not both.

400

visiIds, filters are missing, exactly one parameter must be provided

Add visiIds or filters.

400

projectId is missing, layout is missing or layout does not have a valid value

Include projectId, and set layout to full_details, summary or list.

400

filters[status] is not a filter

Check the filter’s name against Filters above. Names are case-sensitive.

400

filters[statuses] does not have a valid value

Use only the values listed for that filter in Filters above.

400

filters[assigneeId], filters[assigneeType] provide all or none of parameters

Send assigneeId and assigneeType together.

401

Missing API Token

Add the Authorization: Bearer header.

403

Write scope is required to access this endpoint

Request your token with scope=read write, using credentials created with the Read and write scope.

403

Exports are not enabled for this company

Contact Visibuild support to turn on exports for your company.

404

Project not found

Check projectId, and that your credentials can access the project.

404

Visi not found

One of the visiIds doesn’t exist or isn’t visible to your company. Remove it and try again.

404

Export not found

Check the export ID you’re polling. Exports made in the Visibuild app can’t be read through the API.

422

An export can include at most 5,000 Visis

Narrow your filters, or split the export into several requests.

422

No Visis match this request

Your filters matched no Visis. Loosen them, or check you’re not leaving out the nested or archived Visis you want.

429

Rate limit exceeded. Max 10 requests per minute.

Wait for the number of seconds in the Retry-After header, then try again.

For the full endpoint reference, see the Core API documentation.

Did this answer your question?