On this page
Specifications
Product library
Exports
Avoice API
The Avoice API lets a practice take a complete, scheduled copy of everything it holds in Avoice: specifications and their full revision history, materials, schedules, uploaded documents, drawings, generated outputs, QA and contract administration records, and the metadata that ties them together.
The API is organised around REST. It accepts and returns JSON, uses standard HTTP response codes and bearer authentication, and is served over HTTPS only. Version 1 is read-only and covers:
- Specifications: every specification, its sections and clauses, TBC flags, and each issued revision.
- Product library: your practice's products, categories, and suppliers, and the products selected on each project.
- Schedules: finishes, FF&E, door, and other schedules on a project.
- Exports: a complete backup of the whole practice as a downloadable archive, for scheduled backups.
Every request is scoped to a single practice. There is no test mode; the API is read-only, so it is safe to use against live data.
https://api.avoice.co/v1curl https://api.avoice.co/v1/exports \
-H "Authorization: Bearer avk_live_…"Authentication
Authenticate with a practice API key sent as a bearer token in the Authorization header. Requests without a valid key return 401.
- Keys are created and revoked by practice admins under Settings → Integrations → API keys. A key belongs to the practice, not to a person, so it keeps working when staff change.
- A key is shown once, when it is created. Avoice stores only a hash. Keys start with
avk_live_so secret scanners can recognise them. - Each key carries only the permissions you give it:
read:specifications,read:materials(product library),read:schedules,read:projects, andread:exports. Use a separate key for each integration. - To rotate, create a new key, update your script, then revoke the old key. Both work during the overlap. Revocation takes effect immediately.
Key creation, revocation, and every export started or downloaded are recorded in the practice audit log with the key name, IP address, and time.
curl https://api.avoice.co/v1/exports \
-H "Authorization: Bearer avk_live_3kP9…Wq7x"Errors
Avoice uses conventional HTTP status codes. Codes in the 2xx range mean success, 4xx means the request could not be completed as sent, and 5xx means something went wrong on our side. Every response carries a Request-Id header; include it when you contact support.
| Status | Meaning |
|---|---|
| 200 OK | The request succeeded. |
| 202 Accepted | The export job was queued. |
| 302 Found | Redirect to a signed archive download URL. |
| 400 Bad Request | A parameter is missing or invalid. |
| 401 Unauthorized | No valid API key was provided. |
| 403 Forbidden | The key lacks the permission this endpoint needs. |
| 404 Not Found | The resource does not exist in this practice. IDs from other practices always return 404. |
| 409 Conflict | The export is not in a state that allows this action. |
| 410 Gone | The archive has expired and been deleted. |
| 429 Too Many Requests | A rate limit was hit. Wait for Retry-After seconds. |
| 500, 503 | An error on Avoice's side. Safe to retry with backoff. |
Error attributes
typestring- The category of error.
invalid_request_errorauthentication_errorpermission_errorrate_limit_errorapi_error codestring- A stable, machine-readable code such as export_not_ready or archive_expired. Safe to branch on.
messagestring- A human-readable explanation. May change; do not parse it.
paramstring, nullable- The parameter the error relates to, if any.
request_idstring- Matches the Request-Id response header.
{
"error": {
"type": "invalid_request_error",
"code": "project_not_found",
"message": "No project with id 'clx8r1…' exists in this practice.",
"param": "scope.project_ids",
"request_id": "req_7Hq2LmZ0c8Vb"
}
}Rate limits
Limits protect the service rather than meter your usage. A nightly full backup always fits comfortably within them. When a limit is reached the API returns 429 with a Retry-After header; back off for that many seconds rather than retrying immediately.
| Limit | Value |
|---|---|
| Concurrent exports | 1 running per practice, up to 5 queued |
| Exports started | 4 per practice per day |
| Other requests | 100 per minute per key |
| Archive size | No cap. Archives over 20 GB are split into numbered parts |
| Download link lifetime | 24 hours |
| Archive retention | 7 days after completion |
| Job timeout | 6 hours, then failed with a reason |
Enterprise practices can have these limits raised. Contact us with your practice name and the volume you need.
HTTP/1.1 429 Too Many Requests
Retry-After: 120
RateLimit-Limit: 4
RateLimit-Remaining: 0
RateLimit-Reset: 41880Pagination
List endpoints return results newest first, using cursor-based pagination. Pass limit to set the page size and starting_after with the last ID you received to fetch the next page. Keep going while has_more is true.
Parameters
limitinteger- Number of objects to return, between 1 and 100. Defaults to 10.
starting_afterstring- An object ID. Returns the page of results after this object.
ending_beforestring- An object ID. Returns the page of results before this object.
{
"object": "list",
"url": "/v1/exports",
"has_more": true,
"data": [
{ "id": "exp_01J8ZK…", "object": "export", … },
{ "id": "exp_01J8XH…", "object": "export", … }
]
}Versioning
The API version is part of the path (/v1). Additive changes, such as new fields, new optional parameters, or new object types in the archive, can ship at any time, so write your integration to ignore fields it does not recognise.
The archive format has its own date-based schema version (2026-09), recorded in every manifest. Breaking changes to the archive get a new schema version, and the previous version continues to be available for at least 12 months.
The Specification object
A Specification is one specification document on a project, written to a single standard. It holds sections, each section holds clauses, and every issue of the document is kept as a publication. Specification endpoints need the read:specifications permission.
Attributes
idstring- Unique identifier. The same ID is used in the app and in exports.
objectstring- Always "specification".
project_idstring- The project the specification belongs to.
namestring- The document name, such as Architectural Specification.
standardenum- The specification standard and numbering system.
cawsuniclassnatspeccsi scopestring- What the document covers, for example combined or a single package.
section_count, tbc_countinteger- Number of sections, and of clauses still flagged TBC.
latest_publicationobject, nullable- Number, revision, purpose, and date of the most recent issue.
archived_attimestamp, nullable- Set when the specification has been archived.
created_at, updated_attimestamp- When the specification was created and last changed.
{
"id": "clz9p3w7n0001ht6y2u8io4pa",
"object": "specification",
"project_id": "clx8r1k5t0006zx8c7v6bn5ma",
"name": "Architectural Specification",
"standard": "caws",
"scope": "combined",
"section_count": 64,
"tbc_count": 7,
"latest_publication": {
"id": "clzb0e4r1000ajk7l3z5xc9vb",
"number": "SPEC-001",
"revision": "C",
"issued_for": "Tender",
"issued_at": "2026-09-15T16:02:44Z"
},
"archived_at": null,
"created_at": "2026-05-06T10:12:30Z",
"updated_at": "2026-09-19T08:47:02Z"
}List all specifications
GET/v1/specifications
Returns the specifications in your practice, most recently updated first. Supports pagination.
Parameters
projectstring- Only specifications on this project.
standardenum- Only specifications written to this standard.
cawsuniclassnatspeccsi include_archivedboolean- Include archived specifications. Defaults to false.
updated[gte]timestamp- Only specifications changed at or after this time.
curl -G https://api.avoice.co/v1/specifications \
-H "Authorization: Bearer avk_live_…" \
-d project=clx8r1k5t0006zx8c7v6bn5maRetrieve a specification
GET/v1/specifications/:id
Returns a single specification with its summary counts and latest issue. Use List sections for the content.
curl https://api.avoice.co/v1/specifications/clz9p3w7n0001ht6y2u8io4pa \
-H "Authorization: Bearer avk_live_…"List sections and clauses
GET/v1/specifications/:id/sections
Returns the sections of a specification in document order. Pass include=clausesto get each section's clauses with their numbering, text, TBC flags, and linked products.
Parameters
includeenum- Add the clauses to each section.
clauses codestring- Only the section with this code, such as M50 or Ss_25_10_20.
statusenum- Only sections at this stage.
emptydraftingfilledreviewed has_tbcboolean- Only sections with at least one clause flagged TBC.
curl -G https://api.avoice.co/v1/specifications/clz9p3…/sections \
-H "Authorization: Bearer avk_live_…" \
-d include=clauses \
-d code=M50{
"object": "list",
"url": "/v1/specifications/clz9p3…/sections",
"has_more": false,
"data": [
{
"id": "clzc7h2k9000bnm4q8w1er6ty",
"object": "spec_section",
"code": "M50",
"name": "Timber strip/board fine flooring and linings",
"division": "M",
"status": "reviewed",
"version": 6,
"updated_at": "2026-09-12T13:21:09Z",
"clauses": [
{
"number": "M50/110",
"title": "Engineered wood flooring",
"body": "Manufacturer: Northfield Timber Co.\nProduct reference: NF-OAK-190-NB\nFinish: UV oiled…",
"tbc": false,
"product_ids": ["clz4h8q2m0003kd9x7v1bt5ne"]
},
{
"number": "M50/120",
"title": "Underlay",
"body": "Thickness: [TBC — confirm with acoustic engineer]…",
"tbc": true,
"tbc_reasons": ["confirm with acoustic engineer"],
"product_ids": []
}
]
}
]
}List publications
GET/v1/specifications/:id/publications
Returns every issued revision of the specification, newest first: its number, revision letter, purpose of issue, the sections it covered, and a signed link to the PDF or DOCX that was issued. Links are valid for 24 hours.
curl https://api.avoice.co/v1/specifications/clz9p3…/publications \
-H "Authorization: Bearer avk_live_…"{
"object": "list",
"url": "/v1/specifications/clz9p3…/publications",
"has_more": false,
"data": [
{
"id": "clzb0e4r1000ajk7l3z5xc9vb",
"object": "spec_publication",
"number": "SPEC-001",
"revision": "C",
"issued_for": "Tender",
"version": 3,
"format": "pdf",
"section_codes": ["F10", "K10", "M50", "…"],
"published_by": { "id": "clx2u7…", "name": "Priya Shah" },
"file": {
"name": "SPEC-001 Rev C - Tender.pdf",
"size_bytes": 2841120,
"url": "https://files.avoice.co/publications/clzb0e…?sig=…",
"expires_at": "2026-09-24T09:00:00Z"
},
"compared_to_id": "clza8m…",
"created_at": "2026-09-15T16:02:44Z"
}
]
}The Product object
A Product is one entry in your practice's product library: a material, finish, fitting, or system your team has researched and recorded, with its manufacturer, category, technical properties, documents, and suppliers. Product library endpoints need the read:materials permission.
Attributes
idstring- Unique identifier. The same ID is used in the app and in exports.
objectstring- Always "product".
namestring- The product name as it appears in your library.
statusenum- Lifecycle status in the library. Drafts are only returned when you filter for them.
activediscontinuedarchiveddraft manufacturerobject, nullable- The manufacturer's supplier record: id and name.
skustring, nullable- The manufacturer's product code.
internal_codestring, nullable- Your practice's own reference, such as FL-12.
categoryobject, nullable- The product category: id, code (for example a Uniclass Pr code), and name.
subcategoryobject, nullable- The subcategory within the category.
attributesobject- Values for the category's attributes, keyed by attribute. See List product categories for the definitions.
applicationarray of strings- Where the product is used, such as floor, wall, or ceiling.
interior, exteriorboolean- Whether the product is suitable for internal and external use.
dimensions, performance, sustainabilityobject- Structured technical data recorded for the product. Keys vary by category.
documentsarray- Datasheets, certificates, and other documents, each with type, name, and url.
suppliersarray- Suppliers you buy the product through, with the primary supplier marked.
created_at, updated_attimestamp- When the product was added and last changed.
{
"id": "clz4h8q2m0003kd9x7v1bt5ne",
"object": "product",
"name": "Engineered oak flooring, Nordic Blonde, 190 mm",
"status": "active",
"manufacturer": {
"id": "clx7a2k1f0001pl3m9q8wr4ty",
"name": "Northfield Timber Co."
},
"sku": "NF-OAK-190-NB",
"internal_code": "FL-12",
"category": {
"id": "clx7c0d9e0002ab4c5d6e7f8g",
"code": "Pr_35_57_06",
"name": "Timber flooring"
},
"subcategory": { "id": "clx7c1…", "name": "Engineered boards" },
"attributes": {
"finish": "UV oiled",
"wear_layer_mm": "3.5",
"reaction_to_fire": "Cfl-s1"
},
"application": ["floor"],
"color_family": "natural",
"interior": true,
"exterior": false,
"image_url": "https://files.avoice.co/products/clz4h8…/main.jpg",
"summary": "Three-layer engineered oak board, suitable for underfloor heating.",
"dimensions": { "length_mm": 1860, "width_mm": 190, "thickness_mm": 14 },
"sustainability": { "fsc_certified": true, "epd_url": "https://…" },
"documents": [
{ "type": "datasheet", "name": "Technical datasheet", "url": "https://…" }
],
"suppliers": [
{ "id": "clx7s9…", "name": "Hartley Flooring Supplies", "primary": true }
],
"created_at": "2026-03-11T09:42:18Z",
"updated_at": "2026-09-02T14:05:51Z"
}List all products
GET/v1/products
Returns the products in your library, most recently updated first. Supports pagination. To keep another system in sync, pass updated[gte] with the time of your last sync.
Parameters
qstring- Search product name, manufacturer, SKU, and internal code.
categorystring- A category ID or code.
subcategorystring- A subcategory ID.
manufacturerstring- A manufacturer's supplier ID.
supplierstring- Only products available through this supplier.
statusenum- Defaults to active.
activediscontinuedarchiveddraft updated[gte]timestamp- Only products changed at or after this time.
curl -G https://api.avoice.co/v1/products \
-H "Authorization: Bearer avk_live_…" \
-d category=Pr_35_57_06 \
-d "updated[gte]=2026-09-01T00:00:00Z" \
-d limit=50{
"object": "list",
"url": "/v1/products",
"has_more": true,
"data": [
{
"id": "clz4h8q2m0003kd9x7v1bt5ne",
"object": "product",
"name": "Engineered oak flooring, Nordic Blonde, 190 mm",
"status": "active",
"category": { "code": "Pr_35_57_06", "name": "Timber flooring" },
…
},
…
]
}Retrieve a product
GET/v1/products/:id
Returns a single product with all of its attributes, documents, and suppliers. Products belonging to another practice return 404.
curl https://api.avoice.co/v1/products/clz4h8q2m0003kd9x7v1bt5ne \
-H "Authorization: Bearer avk_live_…"List product categories
GET/v1/product_categories
Returns your practice's product categories with their subcategories and the attribute definitions that apply to products in each category. Use it to interpret a product's attributes and to build filters.
curl https://api.avoice.co/v1/product_categories \
-H "Authorization: Bearer avk_live_…"{
"object": "list",
"url": "/v1/product_categories",
"has_more": false,
"data": [
{
"id": "clx7c0d9e0002ab4c5d6e7f8g",
"object": "product_category",
"code": "Pr_35_57_06",
"name": "Timber flooring",
"group": "Finishes",
"subcategories": [
{ "id": "clx7c1…", "name": "Engineered boards", "code": null },
{ "id": "clx7c2…", "name": "Solid boards", "code": null }
],
"attributes": [
{ "key": "finish", "label": "Finish", "type": "text" },
{ "key": "reaction_to_fire", "label": "Reaction to fire", "type": "text" }
]
}
]
}List suppliers
GET/v1/suppliers
Returns the manufacturers and suppliers in your library with their contacts. Supports pagination.
Parameters
company_typeenum- Filter by the kind of company.
manufacturersupplierboth qstring- Search by company name.
curl -G https://api.avoice.co/v1/suppliers \
-H "Authorization: Bearer avk_live_…" \
-d company_type=manufacturer{
"object": "list",
"url": "/v1/suppliers",
"has_more": false,
"data": [
{
"id": "clx7s9f3a0004qw2e1r5ty8ui",
"object": "supplier",
"name": "Hartley Flooring Supplies",
"company_type": "supplier",
"website": "https://…",
"product_count": 214,
"contacts": [
{ "name": "Sam Hartley", "email": "sam@…", "role": "Account manager" }
]
}
]
}List a project's products
GET/v1/projects/:id/products
Returns the products selected on a project, with their status on that project, notes, who added them, and the specification sections they appear in. Needs both read:materials and read:projects.
Parameters
statusenum- Status of the product on this project. Returns all statuses by default.
proposedactiverejectedsuperseded
curl -G https://api.avoice.co/v1/projects/clx8r1k5t0006zx8c7v6bn5ma/products \
-H "Authorization: Bearer avk_live_…" \
-d status=active{
"object": "list",
"url": "/v1/projects/clx8r1…/products",
"has_more": false,
"data": [
{
"object": "project_product",
"project_id": "clx8r1k5t0006zx8c7v6bn5ma",
"status": "active",
"notes": "Ground floor living areas. Client approved sample 04/09.",
"added_at": "2026-09-04T11:20:07Z",
"added_by": { "id": "clx2u7…", "name": "Priya Shah" },
"spec_sections": ["M50"],
"product": {
"id": "clz4h8q2m0003kd9x7v1bt5ne",
"object": "product",
"name": "Engineered oak flooring, Nordic Blonde, 190 mm",
…
}
}
]
}List a project's schedules
GET/v1/projects/:id/schedules
Returns the schedules on a project, such as finishes, FF&E, door, and lighting schedules, with their item counts. Schedule endpoints need the read:schedules permission.
Parameters
typeenum- Only schedules of this type.
finishesffefurniturelightingdoorcustom
curl -G https://api.avoice.co/v1/projects/clx8r1…/schedules \
-H "Authorization: Bearer avk_live_…" \
-d type=finishesRetrieve a schedule
GET/v1/schedules/:id
Returns a schedule with its columns and rows. Each row is an item in an area, with its values and any products from your product library assigned to it.
curl https://api.avoice.co/v1/schedules/clz2f6d4s0008vb3n1m7qx9lk \
-H "Authorization: Bearer avk_live_…"{
"id": "clz2f6d4s0008vb3n1m7qx9lk",
"object": "schedule",
"project_id": "clx8r1k5t0006zx8c7v6bn5ma",
"name": "Internal finishes",
"type": "finishes",
"columns": ["Area", "Element", "Product", "Finish", "Notes"],
"rows": [
{
"id": "clz2g1…",
"area": "Ground floor living",
"element": "Floor",
"values": { "finish": "UV oiled", "notes": "Run boards east–west" },
"products": [
{ "id": "clz4h8q2m0003kd9x7v1bt5ne", "name": "Engineered oak flooring, Nordic Blonde, 190 mm" }
]
}
],
"updated_at": "2026-09-18T15:30:12Z"
}The Export object
An Export is a background job that builds an archive of your practice data. Large practices can take a while to export, so the job runs on its own queue and never slows down Avoice for your team.
Attributes
idstring- Unique identifier, prefixed exp_.
objectstring- Always "export".
statusenum- Where the job is in its lifecycle.
queuedrunningreadyfailedcanceledexpired scopeobject- What the export covers.
typeispracticefor everything, orprojectswith a list ofproject_ids. sincetimestamp, nullable- If set, the archive only holds records created, changed, or deleted after this time.
include_filesboolean- Whether binary files are included alongside the structured data.
schema_versionstring- The archive schema version used for this export.
created_byobject- The API key or admin who started the export.
progressobject- Record and file counts, total and done, updated while running.
partsarray- Once ready: one entry per archive part with its number, size, and SHA-256 checksum.
expires_attimestamp, nullable- When the archive will be deleted. After this the status becomes expired.
failure_reasonstring, nullable- Set when the status is failed.
{
"id": "exp_01J8ZK4V7Q2M9D3HX6T5RWN0BC",
"object": "export",
"status": "ready",
"scope": {
"type": "practice",
"project_ids": null
},
"since": null,
"include_files": true,
"format": "json",
"schema_version": "2026-09",
"created_at": "2026-09-23T02:00:04Z",
"started_at": "2026-09-23T02:00:09Z",
"completed_at": "2026-09-23T02:41:37Z",
"created_by": {
"type": "api_key",
"id": "key_01J7Y2B8C4M1N6P3Q5R7S9T0VW",
"name": "Nightly backup"
},
"progress": {
"records_total": 184213,
"records_done": 184213,
"files_total": 9412,
"files_done": 9412
},
"size_bytes": 31874520113,
"parts": [
{
"number": 1,
"size_bytes": 21474836480,
"sha256": "9f2c41d0b7e3…a61e"
},
{
"number": 2,
"size_bytes": 10399683633,
"sha256": "0c7d88e2f415…3b90"
}
],
"expires_at": "2026-09-30T02:41:37Z",
"failure_reason": null
}Create an export
POST/v1/exports
Starts a new export job and returns it with status queued. Poll Retrieve an export until the status is ready.
Parameters
scopeobject- Defaults to
{"type": "practice"}, the whole practice including archived projects. Use{"type": "projects", "project_ids": [...]}to export selected projects only. sincetimestamp- ISO 8601. Export only what changed after this time, including deletions, for incremental backups.
include_filesboolean- Include uploaded files and generated documents. Defaults to true. Set false for a fast metadata-only export.
formatenum- Archive data format. Only json is available in v1.
json
curl https://api.avoice.co/v1/exports \
-H "Authorization: Bearer avk_live_…" \
-H "Content-Type: application/json" \
-d '{
"scope": { "type": "practice" },
"include_files": true
}'{
"id": "exp_01J8ZK4V7Q2M9D3HX6T5RWN0BC",
"object": "export",
"status": "queued",
"scope": { "type": "practice", "project_ids": null },
"since": null,
"include_files": true,
"format": "json",
"created_at": "2026-09-23T02:00:04Z",
…
}Retrieve an export
GET/v1/exports/:id
Returns an export by ID, with live progress while it runs. Once the status is ready, the response includes the parts to download and their checksums. Polling once a minute is plenty.
curl https://api.avoice.co/v1/exports/exp_01J8ZK4V7Q2M9D3HX6T5RWN0BC \
-H "Authorization: Bearer avk_live_…"{
"id": "exp_01J8ZK4V7Q2M9D3HX6T5RWN0BC",
"object": "export",
"status": "running",
"progress": {
"records_total": 184213,
"records_done": 92740,
"files_total": 9412,
"files_done": 3118
},
…
}List all exports
GET/v1/exports
Returns the practice's exports, newest first, whether they were started through the API or by an admin in Avoice. Supports pagination.
Parameters
statusenum- Only return exports with this status.
queuedrunningreadyfailedcanceledexpired created[gte]timestamp- Only return exports created at or after this time.
curl -G https://api.avoice.co/v1/exports \
-H "Authorization: Bearer avk_live_…" \
-d limit=5 \
-d status=readyDownload an export
GET/v1/exports/:id/download
Redirects to a signed, time-limited URL for one archive part. The link is valid for 24 hours and only for the key that requested it; call this endpoint again for a fresh link. Archives are deleted 7 days after completion, after which this returns 410.
Parameters
partinteger- Which part to download, starting at 1. Defaults to 1.
Compare the SHA-256 of each downloaded part with the sha256 listed in the export's parts to confirm the download is intact.
curl -L -o backup.part1.zip \
"https://api.avoice.co/v1/exports/exp_01J8ZK…/download?part=1" \
-H "Authorization: Bearer avk_live_…"HTTP/1.1 302 Found
Location: https://exports.avoice.co/exp_01J8ZK…/part-1.zip?sig=…
Cache-Control: no-storeCancel or delete an export
DELETE/v1/exports/:id
Cancels a queued or running export, or deletes the stored archive of a finished one before it expires. Use it once your own copy is safely stored if you would rather not leave archives on Avoice for the full 7 days.
curl -X DELETE https://api.avoice.co/v1/exports/exp_01J8ZK… \
-H "Authorization: Bearer avk_live_…"{
"id": "exp_01J8ZK4V7Q2M9D3HX6T5RWN0BC",
"object": "export",
"status": "canceled",
"deleted": true
}Archive layout
Each export is a zip archive, split into numbered parts above 20 GB. The layout follows the way your work is organised in Avoice: practice-wide data at the top, then one folder per project.
- Structured data is JSON. Large collections, such as spec sections and audit events, are JSON Lines (
.jsonl, one record per line) so they can be streamed. - Every record keeps the same ID it has in Avoice, and relationships are expressed by ID, never by name. That is what makes an archive restorable and comparable between backups.
- Files are included in their original format under their original names, with their metadata and version history in
index.json. - Specifications are exported twice. The structured sections and clauses, with full version history, are for restore and comparison. Each issued revision is also included as the PDF or DOCX that was actually issued, as the record of what went out.
- Records deleted within your retention window appear with a
deleted_attimestamp, so an incremental export tells you what was removed as well as what changed. - Files that live in a connected cloud drive (Google Drive, SharePoint, Dropbox) are exported as metadata and source path rather than a second copy, since you already hold them.
avoice-export-exp_01J8ZK…/
├── manifest.json
├── schema/ JSON Schema per type
├── practice/
│ ├── organization.json
│ ├── users.json name, email, role
│ ├── teams.json
│ └── settings.json
├── library/
│ ├── clauses.json clause library, masters
│ ├── materials.json Material Room
│ └── suppliers.json
├── audit/
│ └── events.jsonl
└── projects/{project_id}/
├── project.json
├── specifications/{specification_id}/
│ ├── specification.json
│ ├── sections.jsonl
│ ├── section-versions.jsonl
│ └── publications/ every issued revision
│ ├── publications.json
│ └── P01-rev-C.pdf
├── materials.json
├── schedules/
├── documents/
│ ├── index.json metadata, versions
│ └── files/ original uploads
├── drawings/ register, revisions
├── outputs/ generated documents
├── qc/ checks, findings
├── rfis.json
├── submittals.json
└── decisions.json decision logThe manifest
Every archive starts with manifest.json. It records what the export covered, when it was generated, how many records of each type it holds, and a SHA-256 checksum for every file. Together with the JSON Schema files in schema/, it lets you show an auditor exactly what a backup contains and prove that nothing was altered.
{
"schema_version": "2026-09",
"export_id": "exp_01J8ZK4V7Q2M9D3HX6T5RWN0BC",
"organization_id": "clx2k9v3p0000abcd1234efgh",
"scope": { "type": "practice", "project_ids": null },
"since": null,
"generated_at": "2026-09-23T02:41:12Z",
"counts": {
"projects": 142,
"specifications": 318,
"spec_sections": 20417,
"spec_publications": 1266,
"materials": 5381,
"schedule_items": 41209,
"documents": 9412,
"drawings": 7730,
"qc_findings": 12884,
"rfis": 961,
"submittals": 1418,
"decisions": 2206,
"audit_events": 80137
},
"files": [
{
"path": "projects/clx8r1…/documents/files/A-201 Plans.pdf",
"size_bytes": 4812201,
"sha256": "5b1e0c9a…7d42"
}
]
}What is included
An export covers the practice's specification, project, and library data inside your contracted retention window.
| Area | Included |
|---|---|
| Practice | Practice profile, users (name, email, role), teams, settings, practice standards |
| Projects | Project records, members, status, and visibility, including archived projects |
| Specifications | Specifications, sections and clauses with every saved version, attention and TBC flags, and each issued revision. NATSPEC, Uniclass, CAWS, and CSI formats |
| Library | Clause library and practice masters, Material Room records, manufacturers, suppliers |
| Materials | Project materials and their links to spec sections and schedules |
| Schedules | Finishes, FF&E, furniture, door, lighting, and custom schedules; schedules of works |
| Documents | Uploaded files with versions and categories; drawing register, revisions, and issue history |
| Outputs | Generated documents and reports, exported specifications |
| QA and CA | Drawing QC checks and findings, RFIs, submittals, project decision log |
| Audit | Specification edit history and export and API key events |
Not included:passwords and sign-in data, email and chat content, practice management records (clients, fees, timesheets, invoices), and Avoice's own models, prompts, and regulation library.
Security
An export key can read a practice's entire body of work, so the export path is locked down accordingly.
- Practice isolation. Every query is bound to the practice that owns the key. IDs belonging to another practice return
404, so they cannot be probed. - Admin control. Only practice admins can create keys. Keys can be named, given an expiry date, and revoked at any time.
- Encryption. TLS 1.2 or higher in transit. Archives are encrypted at rest and built and stored in the same UK region as your practice data.
- Short-lived downloads. Download links expire after 24 hours and archives are deleted after 7 days, or sooner if you delete them.
- Audit trail. Every key event, export, and download is written to your practice audit log with key name, IP address, and time.
Scheduled backup script
A complete nightly backup in about 40 lines of Python. It starts a full practice export, waits for it to finish, downloads every part, and checks each checksum before reporting success.
- Install the one dependency with
pip install requests. - Set
AVOICE_API_KEYto a key withread:exports, and optionallyAVOICE_BACKUP_DIR. - Schedule it with cron or Windows Task Scheduler, for example at 02:00 every night.
For large practices, run a full export weekly and pass since set to the previous run's created_at on the other nights for a faster incremental export.
Questions or a higher limit? Email hello@avoice.co.
"""Nightly Avoice backup. Run from cron, e.g. 0 2 * * * python avoice_backup.py"""
import hashlib, os, pathlib, time
import requests
API = "https://api.avoice.co/v1"
KEY = os.environ["AVOICE_API_KEY"] # avk_live_…
OUT = pathlib.Path(os.environ.get("AVOICE_BACKUP_DIR", "./avoice-backups"))
s = requests.Session()
s.headers["Authorization"] = f"Bearer {KEY}"
def call(method, path, **kw):
while True:
r = s.request(method, API + path, timeout=60, **kw)
if r.status_code == 429:
time.sleep(int(r.headers.get("Retry-After", "60")))
continue
r.raise_for_status()
return r
# 1. Start a full practice export.
export = call("POST", "/exports", json={"scope": {"type": "practice"}}).json()
print("started", export["id"])
# 2. Poll until it is ready (large practices can take a while).
while export["status"] in ("queued", "running"):
time.sleep(60)
export = call("GET", f"/exports/{export['id']}").json()
if export["status"] != "ready":
raise SystemExit(f"export {export['status']}: {export['failure_reason']}")
# 3. Download every part and verify its checksum.
target = OUT / export["completed_at"][:10]
target.mkdir(parents=True, exist_ok=True)
for part in export["parts"]:
path = target / f"{export['id']}.part{part['number']}.zip"
digest = hashlib.sha256()
with call("GET", f"/exports/{export['id']}/download",
params={"part": part["number"]}, stream=True) as r, open(path, "wb") as f:
for chunk in r.iter_content(chunk_size=1 << 20):
f.write(chunk)
digest.update(chunk)
if digest.hexdigest() != part["sha256"]:
raise SystemExit(f"checksum mismatch on part {part['number']}")
print("saved", path)