Typed Metadata Values and Attachments
Business object metadata properties (metadata on GET/PATCH /v1/twin/{contextId}/object/{objectId}/metadata) are not limited to plain strings, numbers, and booleans. A property value can also be a tagged object that carries a special _p3d_type field, used to represent an external link or an uploaded file attachment. This page documents that value union and the upload flow behind the attachment type.
The metadata value union
Section titled “The metadata value union”Each property inside a metadata section is one of:
| Value kind | Shape |
|---|---|
| Plain string | "A short text" |
| Plain number | 42 |
| Plain boolean | true |
| External link | { "url": "...", "text": "...", "_p3d_type": "urn:primitive:obj:external-link" } |
| Attachment | { "name": "...", "id": "...", "_p3d_type": "urn:primitive:obj:attachment" } |
_p3d_type is the discriminator: its literal value tells you which of the two tagged shapes you are looking at. Both tagged shapes are otherwise plain objects, and no extra endpoint is needed to read them; they come back inline wherever metadata is returned.
{ "Section name": { "name": "A short text", "numeric value": 42, "toggle name": true, "documentation link": { "url": "https://doc.prevu3d.com/", "text": "https://doc.prevu3d.com/", "_p3d_type": "urn:primitive:obj:external-link" }, "landing image": { "name": "prevu_logo.jpg", "id": "80559c9a-39ac-4e22-af81-7d7395f45673", "_p3d_type": "urn:primitive:obj:attachment" } }}External link (urn:primitive:obj:external-link)
Section titled “External link (urn:primitive:obj:external-link)”| Field | Type | Notes |
|---|---|---|
url | string | The link target. |
text | string | Display text for the link. |
_p3d_type | string | Always urn:primitive:obj:external-link. |
An external link is self-contained: set the object directly on a metadata write, no upload involved.
Attachment (urn:primitive:obj:attachment)
Section titled “Attachment (urn:primitive:obj:attachment)”| Field | Type | Notes |
|---|---|---|
name | string | Original file name. |
id | string | The attachment’s fileId, obtained from the upload flow below. |
_p3d_type | string | Always urn:primitive:obj:attachment. |
Unlike the external link, an attachment value references a file that must exist first. Uploading it is a separate, multi-step call sequence, covered next.
Attaching a file to metadata
Section titled “Attaching a file to metadata”Getting a file referenced as an attachment value is a three-step sequence: initiate the upload, upload the parts to S3, finalize it. Then use the returned file id as the id field of an attachment value on your next metadata write.
| Method | Path | Scope | Purpose |
|---|---|---|---|
POST | /v1/twin/{contextId}/object/attachments | write:asset | Initiate an attachment upload |
GET | /v1/twin/{contextId}/object/attachments/{fileId}/refresh/{startIndex} | write:asset | Reissue expired part URLs |
POST | /v1/twin/{contextId}/object/attachments/{fileId}/finalize | write:asset | Finalize the upload |
GET | /v1/twin/{contextId}/object/attachments/{fileId}/download | read:asset | Get a signed download URL |
DELETE | /v1/twin/{contextId}/object/attachments/{fileId} | write:asset | Remove an attachment |
contextId is the twin, site, or draft the attachment belongs to, the same id you use for business object search.
The upload itself is the same generic initiate → upload parts → finalize mechanic used elsewhere in the API (multipart, presigned S3 URLs, refreshable if they expire). See the shared multipart upload reference for that mechanic and its part-numbering footgun. This section covers only what’s specific to attachments.
Step 1: initiate
Section titled “Step 1: initiate”POST {api_url}/v1/twin/{contextId}/object/attachmentsAuthorization: Bearer {access_token}Content-Type: application/json
{ "fileName": "prevu_logo.jpg", "fileSize": 84213, "mimeType": "image/jpeg" }{ "fileId": "80559c9a-39ac-4e22-af81-7d7395f45673", "parts": 1, "urls": [ { "partNumber": 1, "url": "https://s3…/part-1?…" } ]}Step 2: upload the parts
Section titled “Step 2: upload the parts”PUT each chunk to its presigned url, keeping the returned ETag per part, exactly as described in the shared multipart upload reference.
Step 3: finalize
Section titled “Step 3: finalize”POST {api_url}/v1/twin/{contextId}/object/attachments/{fileId}/finalizeAuthorization: Bearer {access_token}Content-Type: application/json
{ "parts": [ { "partNumber": 1, "etag": "\"a1b2c3d4e5f6\"" } ] }{ "fileId": "80559c9a-39ac-4e22-af81-7d7395f45673", "fileName": "prevu_logo.jpg", "size": 84213}Step 4: reference the file in metadata
Section titled “Step 4: reference the file in metadata”Take the fileId from the finalize response and set it as the id of an attachment value on your next metadata write:
PATCH {api_url}/v1/twin/{contextId}/object/{objectId}/metadataAuthorization: Bearer {access_token}Content-Type: application/json
{ "metadata": { "Section name": { "landing image": { "name": "prevu_logo.jpg", "id": "80559c9a-39ac-4e22-af81-7d7395f45673", "_p3d_type": "urn:primitive:obj:attachment" } } }}A metadata write takes the full metadata object (new, updated, and removed sections and properties together), not a single-property patch, so include every section and property you want to keep alongside the new attachment reference.
Reading tagged values back
Section titled “Reading tagged values back”GET /v1/twin/{contextId}/object/{objectId}/metadata returns each property exactly as it was written: a plain string/number/boolean, or one of the two tagged objects above with _p3d_type intact. There is no separate “resolve” step for reading a tagged value; the object itself, including the attachment’s id, comes back inline in the metadata response.
To get the file’s bytes, pass that id as fileId to the download endpoint below.
Downloading and removing an attachment
Section titled “Downloading and removing an attachment”GET {api_url}/v1/twin/{contextId}/object/attachments/{fileId}/downloadAuthorization: Bearer {access_token}{ "url": "https://d111111abcdef8.cloudfront.net/…" }The returned URL is a signed CloudFront link, valid for roughly 4 hours. Fetch a fresh one if it expires.
To remove an attachment’s underlying file:
DELETE {api_url}/v1/twin/{contextId}/object/attachments/{fileId}Authorization: Bearer {access_token}Attachment limits
Section titled “Attachment limits”Attachment storage rides on the same per-organization storage allocation as every other file in RealityConnect: there is no separate “attachment quota.”
| Question | Answer |
|---|---|
| Maximum size of a single attachment | No dedicated size cap is enforced. The practical ceiling is your organization’s remaining storage headroom. |
| Maximum attachments per object | No dedicated count cap is enforced. A metadata section can hold any number of attachment-typed properties. |
| Total per twin or per organization | There is no separate attachment quota. Every attachment is stored as a regular file and counts toward your organization’s overall storage allocation, the same counter every other uploaded file uses. StorageLimitExceeded (see Error Codes) is returned once usage crosses your organization’s allocated storage. That allocation is plan-specific, so there is no single number to publish here; check with your Prevu3D account team for your organization’s figure. |
| Accepted file types | Any mimeType you send is accepted; there is no allow-list to satisfy. |
| Same file attached to several objects | Stored once per upload, not once per reference. Uploading identical bytes twice (two separate initiate → finalize sequences) creates two separate stored files; there is no automatic deduplication. To attach one shared document to several objects without storing it twice, upload it once and reuse the returned id in each object’s attachment value instead of re-uploading. |
What’s next?
Section titled “What’s next?”- Shared multipart upload reference for the generic initiate/upload/finalize mechanic.
- Searching Business Objects and Nodes for how
contextIdis resolved. - See the API reference for full request and response schemas per operation.