Overview
A Primary Offering, the contract-governed first sale of a new asset to investors, binds its terms to one hash. The platform derives that hash from the terms documents the issuer uploaded, so no caller supplies it. When a sale has a terms hash, an investor must acknowledge it on chain before the first purchase, and a purchase without that acknowledgement reverts with TermsNotAccepted. A sale without terms documents has no acknowledgement step.
The hash is fixed during setup. The sale contract accepts it once, so a second set call reverts with TermsAlreadySet. Upload every terms document before you set the hash.
For the request and response fields of each route, use the v2 API explorer.
How the terms hash is derived
The terms set of a sale is its latest documents with documentType terms. A terms document is always public, so an investor can read it before they have any other access to the sale.
- Each document has a SHA-256 file hash, computed when the upload is confirmed. The sale document list returns it as
fileHash. - The platform sorts the lowercase hex file hashes in ascending order and concatenates their 32-byte values.
- The terms hash is the keccak256 of those concatenated bytes, not of the hex text.
The order of upload does not change the result. You can compute the same hash from the same files outside the platform.
The sale read returns three terms fields:
Once termsHash is set, the platform refuses to add or remove a terms document, so termsDocumentsHash stays equal to termsHash.
Issuer flow
The issuer needs the Offering admin role on the sale. Create the sale first: a document belongs to a sale address, which exists only after create. The create body takes no termsHash.
POST /api/creates the sale in setup.v2/ addons/ token- sales POST /api/returns a presigned upload URL. Sendv2/ addons/ token- sales/ {sale Address}/ document- uploads documentandType: "terms" visibility: "public". Upload the file to that URL.POST /api/confirms the upload and stores the file hash.v2/ addons/ token- sales/ {sale Address}/ documents - Repeat steps 2 and 3 for each terms document.
PUT /api/sends the derived hash to the sale contract. From a signed-in session the call needs wallet verification, like other sale writes. A call sent with an API key needs none. Sendv2/ addons/ token- sales/ {sale Address}/ terms expectedTermsHash, thetermsDocumentsHashyou showed the signer. The field is optional, and when you send it and the documents changed since, the call is refused with422and records nothing.
curl -X PUT "https://your-instance.example.com/api/v2/addons/token-sales/0x5FbDB2315678afecb367f032d93F642f64180aa3/terms" \
-H "X-Api-Key: sm_dalp_..." \
-H "Prefer: wait=30" \
-H "Content-Type: application/json" \
-d '{
"expectedTermsHash": "0x6c1f0e4a9b2d7c38e5f1a0b4d9c2e7f3a8b6d1c4e9f2a7b3c8d5e0f1a6b2c9d4"
}'A settled call answers 200 with the transaction that recorded the hash:
{
"data": { "transactionHash": "0x2b7e9c4d1a6f3e8b5c0d7a2f9e4b1c6d3a8f5e2b9c4d7a1e6f3b8c5d2a9e4f7b" },
"meta": {
"txHashes": ["0x2b7e9c4d1a6f3e8b5c0d7a2f9e4b1c6d3a8f5e2b9c4d7a1e6f3b8c5d2a9e4f7b"],
"transactionId": "0198f4b2-7c3d-7e1a-9b5f-2d4c6e8a0b1c"
},
"links": { "self": "/v2/addons/token-sales/0x5FbDB2315678afecb367f032d93F642f64180aa3/terms" }
}Without Prefer: wait the call answers 202 with transactionId, status and statusUrl. Follow statusUrl until the transaction is final.
Uploading, replacing and removing a terms document needs the Offering admin role. Other sale documents only need a sale management role, so a Funds manager can still manage the brochure or FAQ.
The set-terms call is refused when the sale has no terms document, when the terms are already set or a set-terms transaction is still in flight, and after the sale leaves setup. Adding or removing a terms document is refused with 409 settlement. once the terms are set, while a set-terms or activation transaction is in flight, and after the sale leaves setup. A terms group can only be replaced by another terms document.
Activate the sale only after the terms are set. Activating a setup sale whose uploaded terms documents have no recorded hash is refused with 422.
In the Console, upload the terms documents on the sale's “Marketing” tab and choose Set terms in the Actions menu or on the overview's terms tile. The create form takes no terms document.
Investor flow
An integration, such as an investor portal, follows four calls.
- List.
GET /api/lists the sale documents the caller may see. Keep the items withv2/ addons/ token- sales/ {sale Address}/ documents documentTypetermsandisLatesttrue. The sale read returns the same set astermsDocuments. - Download.
POST /api/returns a download URL for one document, valid for one hour, with itsv2/ addons/ token- sales/ {sale Address}/ documents/ {document Id}/ downloads expiresAt. - Acknowledge.
POST /api/records the acknowledgement on chain for the caller's wallet. The sale read then returnsv2/ addons/ token- sales/ {sale Address}/ term- acknowledgements has.Acknowledged Terms: true - Buy.
POST /api/submits the purchase.v2/ addons/ token- sales/ {sale Address}/ purchases
The acknowledgement takes no field other than walletVerification, which a signed-in session sends and an API-key request leaves out:
curl -X POST "https://your-instance.example.com/api/v2/addons/token-sales/0x5FbDB2315678afecb367f032d93F642f64180aa3/term-acknowledgements" \
-H "X-Api-Key: sm_dalp_..." \
-H "Prefer: wait=30" \
-H "Content-Type: application/json" \
-d '{}'{
"data": { "transactionHash": "0x9d4a1f6c3e8b2d7a5f0c9e4b1a6d3f8c5e2b7a9d4f1c6e3b8a5d2f9c4e7b1a6d" },
"meta": {
"txHashes": ["0x9d4a1f6c3e8b2d7a5f0c9e4b1a6d3f8c5e2b7a9d4f1c6e3b8a5d2f9c4e7b1a6d"],
"transactionId": "0198f4b3-1a2b-7c3d-8e4f-5a6b7c8d9e0f"
},
"links": { "self": "/v2/addons/token-sales/0x5FbDB2315678afecb367f032d93F642f64180aa3/term-acknowledgements" }
}Wait for the terminal settlement verdict of the acknowledgement before you submit the buy, as Idempotency and async settlement describes.
Both writes refuse when the documents and the recorded hash do not line up:
- The acknowledgement is refused with
422when the sale's current terms documents differ from the hash recorded on the sale contract. The investor would otherwise accept terms that are not the documents shown. - The purchase is refused with
422when the sale has terms documents but no recorded hash. - Either call answers with a retryable
503when the terms documents or the recorded hash cannot be read.
A sale without terms documents is not affected by these checks.
The platform does not check that the investor opened a document before they acknowledge. The acknowledgement proves that the wallet accepted the hash, not that the investor read the files. An integration that must show the documents first enforces that in its own flow.
Errors
Next steps
- Primary Offering for the sale lifecycle the terms belong to.
- Idempotency and async settlement for waiting on the acknowledgement's settlement before the purchase.
- Files for how the platform serves stored files.