Skip to main content

Read files and object storage

Read public branding assets with no credential, KYC and asset documents through their own routes, and any private file as a platform administrator.

Overview

A backend integration reads and writes DALP files through domain routes that accept an API key: the asset document routes under /api/v2/tokens/{assetId}/documents, and the KYC document upload and download routes. The /dalp/* routes serve public branding assets to anyone, and /dalp/private/* serves stored private files to a platform administrator's signed-in Console session only and refuses an API key.

Choose public or private

Public assets need no credential. DALP allows public responses to be cached for one year. Private responses require a session and use headers that forbid caching. Keep KYC evidence and other sensitive documents private.

Deleting or moving a public asset cannot recall copies that clients already downloaded. If a sensitive file was exposed, treat those copies as outside the platform's control.

Tenancy in object storage

Object storage is one bucket per deployment. Writes made through an organisation's storage scope carry the key prefix org_{organizationId}/, and an organisation's public branding assets sit under it too: a logo is stored at org_{organizationId}/logos/... and served from /dalp/org_{organizationId}/logos/.... KYC documents, stored under kyc/{participantId}/..., and the platform-reserved platform/... namespace sit outside that prefix. For a file outside the prefix, the route that serves it decides who may read it.

One interface sits in front of the provider implementations a deployment can select: S3, GCP Cloud Storage, Azure, any S3-compatible backend, and a filesystem provider for a self-hosted or air-gapped deployment. A caller sees the same upload-and-download URL contract on all of them, so an integration written against one deployment's storage backend does not need to change for another's.

Public assets

GET /dalp/* and HEAD /dalp/* serve public assets with no authentication. The object key is everything after /dalp/, and DALP only serves keys under a small set of prefixes, after an optional org_{organizationId}/ organisation prefix: logos/, auth/, backgrounds/, favicons/, icons/. The API cannot add a new public prefix. A key outside the allowed prefixes, or one containing a . or .. path segment, returns 404 Not Found instead of a listing or an error that reveals why the request failed.

curl "https://your-instance.example.com/dalp/org_<organizationId>/logos/light/<id>-icon.svg"

DALP sets Cache-Control: public, max-age=31536000, immutable on a public asset response: a one-year cache lifetime that tells every intermediary the bytes at this URL never change. Cache it on your side too. The cost is that a file stored under a public key by mistake stays in caches for a year, because the header tells them not to re-check it. A response also carries ETag and Last-Modified when the stored object has them.

Private files

GET /dalp/private/* and HEAD /dalp/private/* serve a stored private file to a platform administrator only. The object key is everything after /dalp/private/, and it needs at least two segments, such as kyc/{participantId}/.... A signed-in caller without the platform admin role receives 403 Forbidden for every key, whatever resource type or id the key names. Every other caller reads a file through the route of the resource it belongs to:

File
Route
A KYC document
An investor's statement
GET /api/v2/reporting/statements/{statementId}/download, which checks that the statement belongs to one of the caller's wallets.
An asset document
The asset document routes, which check the caller's organisation and the document's visibility.

admin is a single role on the caller's session, not a per-organisation membership: a user is admin or member platform-wide, separate from whatever role they hold inside any one organisation. A platform administrator reads every valid private key, whatever its resource type and whatever id it names. Grant the admin role deliberately, because besides the administrative controls it gives its holder read access to every stored KYC document and every organisation's private files on the deployment.

A private file request does not accept an API key. A platform administrator authenticates with a signed-in Console session (cookie) instead. A backend service has no session cookie to send, so it reads each file through the route of its resource.

curl "https://your-instance.example.com/dalp/private/kyc/user_123/passport.pdf" \
  -H "Cookie: <your session cookie>"

This request returns the stored object only when the session belongs to a platform administrator. Any other signed-in caller receives 403 Forbidden, whether or not an object exists at that key, and reads the document through the KYC document download route.

DALP sets Cache-Control: no-cache, no-store, must-revalidate, plus Pragma: no-cache and Expires: 0, on every private file response. Do not cache a private file in a shared cache or CDN.

Read a KYC document through its domain route

Do not build a /dalp/private/kyc/... URL by hand from a backend integration. KYC documents have their own route that accepts an API key: the KYC document download endpoint. It is a POST because each call issues a new time-limited link rather than reading a fixed one. Call it with X-Api-Key, and it returns a scoped link instead of the file itself:

{
  "data": {
    "downloadUrl": "/api/v2/kyc-profile-versions/.../documents/.../download",
    "fileName": "proof-of-address.pdf",
    "mimeType": "application/pdf",
    "expiresAt": "2027-01-05T15:05:00.000Z"
  },
  "links": { "self": "/api/v2/kyc-profile-versions/.../documents/.../downloads" }
}

The fields you want are under data, as on every other v2 single-resource response.

downloadUrl is always a relative path starting /api/v2/. The path stays gated by a session or API key after expiresAt, so send your credential each time you fetch it. expiresAt is always one hour after the call that issued the link, and it bounds the link only, not your credential. No KYC document is served through a link that anyone holding it can use.

A document that the v1 KYC upload stored before 3.2 as unencrypted is encrypted in place by a background job on the runner, which also deletes the unencrypted object. Until the job reaches it, the same /api/v2/ path serves that document straight from storage, still behind your credential.

Request a fresh link if one stops working rather than assuming it is permanently dead.

For other files, use the download or export operation documented for the owning resource. Do not infer a supported integration route from an object-key prefix alone.

API keys are refused on private file routes

If you send only an X-Api-Key header directly on /dalp/private/*, with no session cookie, you get 401 Unauthorized, the same response as sending no credential at all. The route does not evaluate API keys, so its response is the same for “your key is invalid” and “you sent no key”. If your integration authenticates every other call with an API key, expect this one route to need a different credential path.

Upload with a presigned URL

An asset or token document upload is a two-step, presigned flow. Request an upload URL first. The response carries a PUT method, an objectKey, headers to send with the upload, and an expiresAt fifteen minutes out. Send the file bytes directly to that URL, then confirm with the returned objectKey so DALP records the file against its owning record. An Azure-backed deployment needs one provider-specific header on the direct upload, x-ms-blob-type: BlockBlob. Other providers need none. Request a fresh upload URL if expiresAt passes or storage rejects the headers you sent.

A KYC document upload does not use a presigned URL. Send it to POST /api/v2/kyc-profile-versions/{versionId}/documents with documentType, fileName, fileSize, mimeType and the file as base64 in fileData, so the platform can validate and encrypt the document before object storage receives it.

The v1 KYC API still offers a presigned upload. Its link points at an object key that ends in /upload under the version, and the client sends the file there unencrypted. The confirm call must name that upload key, and any other key is refused. The platform requires a draft version, reads the uploaded object and refuses it when it is over 25 MiB, when its size differs from the declared size, or when its bytes are not the declared type. It then encrypts the file and stores the encrypted copy the same way as a v2 upload, so the document downloads through a relative /api/v2/ path. The platform deletes the uploaded object at the end of that confirm, whether the confirm stored the document, refused it or failed. After a refused or failed confirm, request a new upload link and send the file again. If you never confirm, a job on the runner deletes the uploaded object 30 minutes after the link was issued, which is the 15 minutes the link lasts plus 15 minutes of margin. The file sits unencrypted in object storage until the confirm or that deletion, so upload KYC documents through the v2 route.

A v2 KYC upload repeated with the same Idempotency-Key header replays the first one. The platform derives the document's id from the organisation, the version, the calling user and the key. A retry with the same key, from the same user, on the same version answers with the document the first request stored and stores nothing again, including when two attempts arrive at the same time. A retry with the same key and a different file, whether its bytes, document type, filename, size or media type differ, answers 400, and data.errors names Idempotency-Key. Send a new key for each new file. Without the header, each request stores a new document.

This replay differs from the transaction queue's rules in Idempotency and async settlement. It has no 24-hour window: it holds as long as the document exists. After the document is deleted, the same key stores the file again. The replay answers with the stored document's details, not a stored copy of the first response.

Asset documents

An asset's documents, such as its prospectus, term sheet and holder notices, use the presigned flow. The file goes between the client and object storage, and the Platform API hands out the links and records the document.

Step
Route
Who can call it
List the documents
GET /api/v2/tokens/{assetId}/documents
A caller in the issuing organisation. The list holds only what they may read.
Request an upload link
POST /api/v2/tokens/{assetId}/document-uploads
A wallet with the governance role on the asset.
Confirm the upload
POST /api/v2/tokens/{assetId}/documents
A wallet with the governance role on the asset.
Request a download link
POST /api/v2/tokens/{assetId}/documents/{documentId}/downloads
A caller in the issuing organisation whose level includes the document's visibility.
Delete a version
DELETE /api/v2/tokens/{assetId}/documents/{documentId}
A wallet with the governance role on the asset.

The upload link response sets expiresAt 15 minutes out, and the download link response one hour out. A file is at most 52,428,800 bytes, which is 50 MiB, and is a PDF, JPEG, PNG, WebP, DOCX, XLSX or CSV file. A brand_logo document is a PNG, JPEG or WebP image of at most 5,242,880 bytes, which is 5 MiB. An upload link request for any other file type or size is refused with 400 REQUEST_INVALID (DALP-14261), and data.errors names the rule. The platform stores a brand logo as public whatever visibility the request names. The confirm call also refuses and deletes a brand_logo whose stored size differs from the declared size, whose stored bytes are not the declared image type, or that is over 2048 pixels on either side. The document type must fit the asset type.

The confirm call names the objectKey, the filename, size and type, the document type, the visibility, which is public, holders or restricted, and an optional title and description. To publish a new version of a document, send the document's groupId as replaceGroupId and the same documentType. A confirm that names another documentType answers 404 DALP-0322, the same answer as an unknown group. The platform reads the stored file back, refuses it when its size differs from the size the request declared, computes the SHA-256 fingerprint from the stored bytes and records the document. A confirmed holders document emits the token.document.published webhook event.

Only callers in the issuing organisation, or in the organisation whose own system registers the asset, reach its documents. Within that scope, visibility decides who reads a document. Every caller reads public documents. A caller whose wallet holds the asset, has units allocated or money contributed in one of its primary offerings that were not withdrawn or refunded, or holds any role on the asset also reads holders documents. A caller with the governance or admin role on the asset also reads restricted documents. A download request beyond the caller's level answers 404 with DALP-9486, the same answer as a missing document, so the response does not reveal that the document exists. A superseded or deleted version answers the same way.

Status
Error id
Code
Meaning
400
DALP-0324
TOKEN_DOCUMENTS_DOCUMENT_TYPE_NOT_VALID_ASSET
The document type does not fit the asset type. A caller error: pick a type the asset offers.
400
DALP-0327
TOKEN_DOCUMENTS_INVALID_OBJECT_KEY
The objectKey does not name an object under this asset. A caller error: confirm the key the upload link returned.
403
DALP-9485
TOKEN_ACCESS_UNAUTHORIZED
The wallet that signs the request does not hold the governance role on the asset. Returned by the upload link, confirm and delete routes. A delete answers it whether or not the document exists.
404
DALP-0322
TOKEN_DOCUMENTS_DOCUMENT_GROUP_NOT_FOUND_REPLACEMENT
The replaceGroupId names no document of the requested type on this asset. A caller error.
404
DALP-0326
TOKEN_DOCUMENTS_FILE_NOT_FOUND_STORAGE_UPLOAD_FAILED
The platform could not read the stored object. On a confirm, upload the file again with a new link. On a download, try again, and contact support if it recurs.
404
DALP-9486
TOKEN_DOCUMENTS_READ_DOCUMENT_NOT_FOUND
A download names no live latest version the caller may read. A missing, superseded, deleted or higher-visibility document answers the same way.
404
DALP-0323
TOKEN_DOCUMENTS_DOCUMENT_NOT_FOUND
A delete names no live version on this asset.
422
DALP-0321
TOKEN_DOCUMENTS_DECLARED_FILE_SIZE_DOES_NOT_MATCH_ACTUAL_STORAGE
The stored object is not the size the request declared, for any type other than brand_logo. The platform deletes it. Upload again.
422
DALP-12131
TOKEN_BRAND_LOGO_REJECTED
On confirm, a brand_logo whose stored size differs from the declared size, whose stored bytes are not the declared PNG, JPEG or WebP type, or that exceeds 2048 pixels on a side. The platform deletes it.
503
DALP-9267
TOKEN_WRITE_PERSISTENCE_FAILED
The platform could not record the document. The error is retryable: retry the confirm call once the platform recovers.

Asset documents and holder notices describes the same flow in the Console.

Status codes

These routes answer an error with a short plain-text body, such as Forbidden, and no DALP error envelope or DALP- id, so branch on the HTTP status.

Status
Meaning
200
The object exists and the caller can read it.
400
The path could not be parsed into an object key.
401
No signed-in session on the request.
403
On /dalp/private/*: the session's user is not a platform administrator, or the key has a . or .. segment or fewer than two segments.
404
No object exists at that key. Also returned for a public request whose key is outside the allowed prefixes.
405
A method other than GET or HEAD.
500
An unclassified storage read failure. DALP gives no retry guarantee for it. Investigate a 500 that recurs on the same key.
503
DALP could not reach object storage. Retry once the service recovers.

/dalp/* and /dalp/private/* have no route-specific rate limit. The per-key limit on Authentication and API keys does not apply, because these routes reject API keys or never require one.

Tell a session problem from a scope problem

A 401 and a 403 have different causes. 401 means DALP found no valid session at all: no cookie, an expired one, or a request sent from a context that never signed in. 403 means the session is real and DALP knows who you are, but your user does not hold the platform admin role, or the key has a . or .. segment or fewer than two segments. The platform checks this before it reads storage, so a refused key answers 403 whether or not an object exists there.

A caller without the platform admin role, an organisation owner included, receives 403 on every private key, including a key that names their own user or organisation. Switching the active organisation or signing in again does not change the answer. Read the file through the route of its resource instead: the KYC document download route, the statement download route or the asset document routes.

Next steps

  • Errors covers the error envelope of the Platform API routes, which the file routes do not use.
  • Authentication and API keys covers API-key authentication and its scope, which private file routes do not accept.

Was this page helpful?