Skip to main content

Send an issuer its shareholder list after an auction or at month end

Produce one security's shareholder list at a record block, send it to the issuer, and exchange the register with the Registrar in its own layout.

Overview

DALP produces an issuer's shareholder list itself: after every trade of an auction cycle is final, at the close of the last trading day of each month for securities that trade continuously, and again after a bust. It stores the list for the issuing organisation and emails the issuer's owners, as Have the platform deliver the list to the issuer describes. Each list covers one security at one record block, the chain block whose balances the list reports. Every holder's balance in the list is the on-chain balance after that block.

Use the manual export to read a list on demand, or to produce the month-end list yourself on a deployment that switched the month-end trigger off.

Choose when to run it

Trading model
When the platform runs it
Record block
When you run it
Auction
After every trade of the auction cycle is final.
The block that holds the cycle's last settlement, or any later block before the next trade of that security.
When the auction trigger does not apply to the security, for example an instrument that is not auction-only.
Continuous trading
On the last trading day of the month, after the session closes and the cut-off passes, with the settings in Month-end lists and defaults.
The final block at the cut-off, or the last settled trade block of that session when it is later.
Only when REPORTING_SHAREHOLDER_LIST_MONTH_END is false. Take the last trading day from the venue calendar, then post the last block of that day.

The record block must be final. DALP returns an error for a block later than the newest final block, so the balances at the record block do not change after you send the list. A later bust can still supersede the list, as Corrections describes. Leave the record block out to use the newest final block. The answer names the block it used.

Export from the Console

  1. Open the security, go to “Holders” and select Export in the table toolbar.
  2. Select Holder register…. The sheet opens at the block entered in “As of” on the Holders tab. For a past date it opens at the last final block at or before that date, and shows both. Otherwise it opens at the newest final block. Choose “Block” to enter another record block.
  3. Read the record block and its time, the number of holders, the units held and the total supply. The sheet uses the class-neutral words holder and units because it also lists stablecoins and deposit tokens.
  4. Check that Adds up shows “Yes”.
  5. Choose the Format, which is CSV by default. The download button names the format, as in “Download CSV”, “Download XLSX” or “Download PDF”. The file is read at the record block shown in the summary.

Export shows the register only to organisation owners, platform administrators, and accounts with the Permission manager, Auditor, Compliance manager or System manager role. Everyone else sees “This table as CSV”, which exports the rows and filters on screen.

Export with the Platform API

Read the totals first:

curl "https://your-instance.example.com/api/v2/reporting/shareholder-lists/<tokenAddress>?atBlock=<recordBlock>" \
  -H "X-Api-Key: <your api key>"

The answer carries the totals at the record block:

{
  "data": {
    "chainId": 1,
    "tokenAddress": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
    "name": "Example Share",
    "symbol": "EXS",
    "decimals": 18,
    "recordBlock": "1200",
    "finalBlock": "1240",
    "recordedAt": "2026-09-30T23:59:48.000Z",
    "balancesSince": "2026-01-15T09:30:00.000Z",
    "holderCount": 12,
    "sharesHeld": "1000",
    "totalSupply": "1000",
    "reconciled": true,
    "ownershipBasis": "on-chain-register",
    "formats": ["csv", "xlsx", "pdf"]
  },
  "links": {
    "self": "/api/v2/reporting/shareholder-lists/0x71C7656EC7ab88b098defB751B7401B5f6d8976F?atBlock=1200"
  }
}

data.reconciled is true when the holders' balances add up to data.totalSupply. data.recordBlock is the block the list is read at, and data.finalBlock is the newest final block. data.holderCount counts the holders, and data.sharesHeld is the units they hold. A refused read answers DALP-0753, with the reasons in When DALP cannot produce the list.

Then download the file at that same block. format is csv, xlsx or pdf:

curl "https://your-instance.example.com/api/v2/reporting/shareholder-lists/<tokenAddress>/download?atBlock=<recordBlock>&format=csv" \
  -H "X-Api-Key: <your api key>" \
  -o shareholder-list.csv

To read a list as of a date, first resolve the date to a block. The call answers with the last final block whose time is at or before the time you give, on the network of the request. A time after the newest final block gives the newest final block. A time before the network's first block gives recordBlock: null.

curl "https://your-instance.example.com/api/v2/reporting/record-blocks?at=2026-09-30T23:59:59Z" \
  -H "X-Api-Key: <your api key>"
{
  "data": {
    "chainId": 1,
    "at": "2026-09-30T23:59:59.000Z",
    "recordBlock": "1200",
    "recordedAt": "2026-09-30T23:59:48.000Z",
    "finalBlock": "1240"
  },
  "links": {
    "self": "/api/v2/reporting/record-blocks?at=2026-09-30T23%3A59%3A59.000Z"
  }
}

The answer carries recordBlock, recordedAt, which is the time of that block, finalBlock and chainId. Pass recordBlock as atBlock on the totals and download calls.

Pass atBlock on the download even when you left it out of the first request. The newest final block moves as the chain grows, and a fixed block keeps the file equal to the totals you checked.

Check the list before you send it

  • Adds up must be “Yes”, or reconciled must be true. A “No” means the holders' units do not add up to total supply at the record block, so the list is incomplete or the balance history has not caught up. Do not send it. Read the list again later, and contact support with the security and the record block if it still shows “No”.
  • The number of holders should match the investors you expect after the auction or month.

Send the list

Deliver the CSV or XLSX file through the channel agreed with the issuer, with the PDF as the record of what was sent. Keep the security address and the record block with the sent file, so that the same list can be produced again.

Exchange the register with the Registrar

The Registrar is the issuer's registrar or transfer agent, the party outside DALP that keeps the issuer's register of members. DALP writes the holders in the Registrar's layout and compares the Registrar's own file with the chain. Both steps run on the “Registrar file” tab of the Console's export sheet or through the Platform API, and both read one record block.

Export shows the Registrar file only to organisation owners, platform administrators, and accounts with the Permission manager, Auditor, Compliance manager or System manager role. Every other account sees only “This table as CSV” in the menu.

Export the Registrar file from the Console

  1. Open the security, go to “Holders”, and select “Export” › Registrar file…. The sheet “Export holder register” opens on its “Registrar file” tab.
  2. Set the record block on the Register tab, where “As of” offers “Newest final block”, “Block” with a block number field, and “Date”. The Registrar file tab exports the block the register was read at. Both buttons on the Registrar file tab stay unavailable until the register answers for a block.
  3. Return to the “Registrar file” tab and select Export registrar file. The browser saves registrar-file-<symbol>-<block>.csv.

The file has the columns holderWallet, identityReference, jurisdiction and quantity, one row per wallet holding a balance, sorted by wallet. The holder is identified by wallet address, and quantity is in the security's own units, for example 250.5 for a security with two decimals.

Export the Registrar file with the Platform API

Download it at a fixed block:

curl "https://your-instance.example.com/api/v2/reporting/shareholder-lists/<tokenAddress>/registrar-file?atBlock=<recordBlock>" \
  -H "X-Api-Key: <your api key>" \
  -o registrar-file.csv

Leave atBlock out to read the newest final block. Pass it to keep the file equal to the register you checked, because the newest final block moves as the chain grows.

Reconcile a Registrar file from the Console

  1. Set the record block on the “Register” tab the same way, then open Registrar file.
  2. Select Reconcile registrar file and choose the Registrar's CSV file.
  3. Read the answer. The badge reads “Matches the chain at block” or “Does not match the chain at block” with the block, and beneath it the count of each result and the totals “Registrar file total”, “On-chain total” and “Total supply” in the security's units.
  4. The table under the summary lists every wallet that did not match, with the result, the quantity the file lists and the quantity on chain. A matched wallet opens no row.

An organisation owner or a platform administrator also writes each difference to the custody reconciliation break register. Any other reader gets the same comparison and records nothing.

When the file cannot be read, the alert “The registrar file could not be read” names the problem and the line, and nothing is compared.

Reconcile a Registrar file with the Platform API

Send the Registrar's file back to compare it with the chain at a block. The fileData field is the CSV text, with a header row that names at least holderWallet and quantity:

curl -X POST "https://your-instance.example.com/api/v2/reporting/shareholder-lists/<tokenAddress>/registrar-reconciliations" \
  -H "X-Api-Key: <your api key>" \
  -H "Content-Type: application/json" \
  -d '{"atBlock": "<recordBlock>", "fileData": "holderWallet,quantity\n0x1111111111111111111111111111111111111111,600\n"}'

When the file is read, data._tag is Reconciled and every wallet has an outcome:

outcome
Console result
What it means
matched
No row
The file and the chain list the same quantity for the wallet.
quantity-mismatch
Different quantity
Both list the wallet, with different quantities.
missing-on-chain
Missing on chain
The file lists the wallet, but it holds nothing at the record block.
missing-in-register
Missing in the file
The wallet holds a balance on chain, but the file does not list it.

data.reconciled is true only when every row matched and the file total and the on-chain total both equal data.totalSupply.

When the file cannot be read, data._tag is Rejected, nothing is compared, and data.refusal names the problem (empty-file, unterminated-quote, malformed-quote, missing-column, invalid-wallet, invalid-quantity or duplicate-wallet) with data.line and data.value. Fix the file and send it again.

Look through nominee wallets to their beneficial owners

A nominee, custodian or broker can hold a security in one wallet for many beneficial owners. On chain that wallet is one holder. The nominee declares the owners behind the wallet, and readers of the shareholder list can then read the register looked through to those owners. Deliveries, the Registrar file and dividend payouts keep counting the on-chain register. A dividend read can still report each owner's share of the nominee's payout.

Declare and read them in the Console

  1. Open the security, go to “Holders” and select “Export” › Beneficial owners…. The sheet “Export holder register” opens on its “Beneficial owners” tab. Set the record block on the “Register” tab first: the tab declares and reads at that block, and “File declaration” stays unavailable until the register answers for a block.
  2. Under “Declare beneficial owners”, enter the “Nominee wallet”, select “Choose CSV file” and pick a file with the columns that Declare the owners behind a wallet lists. Select File declaration.
  3. A filed declaration shows Declaration filed at block with the nominee wallet, the owners and units declared, and whether the identities were stored encrypted or were not in the file. A refused one shows “The declaration was not filed” with the reason, the line and the column of the problem. Nothing is stored. A wallet that is not an address is refused before anything is sent.
  4. Under Register by beneficial owner, read the register at the record block: whether it adds up to the total supply, the number of entries, the wallets looked through and the stale wallets. The table has one row per direct holder, declared owner or stale wallet, and “Holding” filters to one kind. The identity columns (“Legal name”, “ID type”, “ID number”, “Investor category”) show only when the register returned them. A stale wallet shows “Stale” on a highlighted row, listed whole, with a notice that a new declaration at a later block looks through it again.
  5. Select Export in the table toolbar to download the rows on screen as CSV, including the identity columns when they are shown.

The Console reads the register only while the “Beneficial owners” tab is open, because every read is recorded in the audit trail. A member files a declaration for a wallet it holds. The organisation owner or a platform administrator can file for any wallet.

Declare the owners behind a wallet

The nominee sends one declaration per security and wallet, for one block. fileData is CSV text with the header ownerReference,jurisdiction,quantity:

  • ownerReference is the nominee's own reference for the owner: 1 to 64 printable characters without spaces, unique in the file.
  • jurisdiction is the owner's ISO 3166-1 alpha-2 country, such as AE.
  • quantity is the units held for the owner, in the security's own units, above zero.

Four optional columns add each owner's identity. Leave them all out and the declaration is exactly ownerReference, jurisdiction and quantity. If the header has any of them it must have legalName, and then every row needs a legalName:

  • legalName is the owner's legal name, at most 200 characters, with no control characters.
  • idType is one of passport, national_id, drivers_license, tax_id, company_registration or lei. Leave it empty for a row without an ID. A row with an idType needs an idNumber, and the other way round.
  • idNumber is the number or registration code, 1 to 64 printable characters without spaces.
  • investorCategory is one of retail, professional, institutional or eligible_counterparty, or empty.
curl -X POST "https://your-instance.example.com/api/v2/reporting/shareholder-lists/<tokenAddress>/beneficial-owner-declarations" \
  -H "X-Api-Key: <your api key>" \
  -H "Content-Type: application/json" \
  -d '{"nomineeWallet": "<wallet>", "atBlock": "<block>", "fileData": "ownerReference,jurisdiction,quantity\nC-1,AE,600\nC-2,SA,400\n"}'

Leave atBlock out to declare at the newest final block. A file holds at most 5000 owners.

A member of the organisation declares for a wallet it holds. The organisation owner or a platform administrator can declare for any wallet, for example from a file the nominee sent. Any other caller is answered as not found.

The declaration is stored only when its quantities add up to the wallet's balance at the block. Then data._tag is Declared, with the declaration's id, asOfBlock, ownerCount and total. Otherwise data._tag is Rejected, nothing is stored, and data.refusal names the problem:

refusal
What it means
total-mismatch
The owners do not add up to the wallet's balance. data.declaredTotal and data.onChainBalance give both.
already-declared
A different declaration for this wallet at this block is stored. Declare the correction at a later block.
empty-file, too-many-lines, unterminated-quote, malformed-quote, missing-column
The file cannot be read as a declaration. For missing-column, data.column names the column.
invalid-owner-reference, invalid-jurisdiction, invalid-quantity, duplicate-owner-reference
A row is wrong. data.line and data.value point at it.
invalid-legal-name, invalid-id-type, missing-id-type, missing-id-number, invalid-id-number, invalid-investor-category
An identity cell is wrong. data.line, data.value and data.column point at it.

Declarations are never changed. Sending the same file for the same wallet and block again answers with the stored declaration. A file that differs only in its identity columns counts as a different declaration.

How the identities are kept

The identity columns are sealed with envelope encryption in the platform database, as one envelope per declaration. They never go on chain, into email, webhooks or indexer data. The declaration's answer does not repeat them: data.withIdentities says whether the file carried them. The owner reference, country and units stay readable as before.

Only reporting readers can read the identities back, through the looked-through register: the organisation owner, holders of the Permission manager, Auditor, Compliance manager or System manager role, and a platform administrator, who runs the platform and reaches every organisation. A member who only files for its own wallet cannot read the identities back, and every read is recorded in the audit trail.

Read the looked-through register

curl "https://your-instance.example.com/api/v2/reporting/shareholder-lists/<tokenAddress>/beneficial-owners?atBlock=<recordBlock>" \
  -H "X-Api-Key: <your api key>"

Reading needs the same access as the shareholder list, and every read is recorded in the audit trail. Each row has a basis:

basis
What the row is
direct
A wallet with no declaration at or before the block, at its whole balance.
declared
One owner of a wallet whose newest declaration at or before the block still adds up to its balance, with its reference and country.
stale
A wallet whose newest declaration no longer adds up to its balance, at its whole balance. Ask the nominee for a new declaration.

A declared row also carries legalName, idType, idNumber and investorCategory when its declaration had identity columns. They are null on direct and stale rows, on declarations filed without identities, and for an idType, idNumber or investorCategory the file left empty.

data.reconciled is true when the rows add up to data.totalSupply. data.ownerCount counts the rows, so two nominees' owners count separately even when they are the same person. data.lookedThroughWallets and data.staleWallets count the wallets split into owners and the wallets left whole as stale.

Read a dividend's entitlements per beneficial owner

To see what each declared owner of a nominee wallet is due from a dividend, name the dividend instead of a block:

curl "https://your-instance.example.com/api/v2/reporting/shareholder-lists/<tokenAddress>/beneficial-owners?dividendId=<dividendId>" \
  -H "X-Api-Key: <your api key>"

The register is read at the dividend's record block, so atBlock is not accepted together with dividendId. The dividend must have its record-date snapshot computed. Otherwise the call answers not found, with the resource dividend. A dividend of another security answers the same way.

Each row gains entitlement, in the payout asset's own units (data.dividend.payoutAsset and data.dividend.payoutDecimals):

  • A declared row holds the owner's share. It applies the dividend's own per-unit rule, units × amountPerShare ÷ one whole unit rounded down, to the owner's units from the declaration.
  • A direct or stale row holds the wallet's own entitlement, exactly what the dividend pays that wallet.

data.dividend.nominees lists each wallet that was split. Its entitlement is the one amount the dividend pays the wallet on chain, allocated is the owners' shares added up, and unallocated is the rounding remainder. The owners' shares plus unallocated always equal entitlement, to the unit. A wallet with a stale or no declaration is not split and not listed. When the dividend pays the wallet less than the owners' shares add up to, as for units the issuer's treasury holds, no owner gets a share and the whole amount is unallocated.

This is a report only. The nominee is still paid on chain as one entitlement and claims it once. The dividend's Merkle root, its approval fingerprint and its payout do not change, and nothing is claimed per owner. Without dividendId, entitlement and data.dividend are null.

Have the platform deliver the list to the issuer

With the triggers on, skip the export and the handoff. The platform produces the list itself, stores the CSV, XLSX and PDF editions, and mails the issuing organisation that the list arrived. Each delivery is bound to one record block.

What starts a delivery

Four triggers create a delivery:

  • auction-settled: the platform opens it once every auction trade of the security has settled, no fill is still waiting to settle, and no settled trade still waits for delivery, at the highest settled fill block or later delivery block. This covers auction-only instruments, including those with a holding period.
  • month-end: the platform opens it for each continuous-book security after the last trading day of a month closes. The delivery records that trading day. Month-end lists and defaults lists the settings.
  • correction: the platform opens it after a bust reverses a settled trade that a delivered list counted. See Corrections.
  • scheduled: you open it when you post a record block through the Console or the Platform API.

A delivery stays pending until its record block is final. A scheduled pass produces each delivery whose block is final. One record block holds one delivery: posting a block that already has one returns that delivery, whichever trigger created it. A delivery whose holders' balances do not add up to total supply also stays pending: the pass records an attempt and a last error, writes no files, sends no mail, and writes no outcome audit until the projection reconciles.

Month-end lists and defaults

A continuous-book security gets its month-end list without any action from you. The platform finds the last trading day of the month, waits for the cut-off, and opens a delivery at the record block. A month stays eligible for 35 days plus the cut-off grace period, so a month missed during a platform outage is still delivered when the platform recovers. After that window the platform opens no month-end delivery for the month. Post its record block yourself, as Schedule with the Platform API describes. A month that already has a delivery opens no second one.

The last trading day is the last session occurrence in the instrument's local calendar month. Collection windows do not count. The cut-off is the session close plus a grace period. The platform opens the delivery only when the session is closed, no trade is waiting to settle, and no fill is queued.

Setting
Default
Config
Month-end list
On
REPORTING_SHAREHOLDER_LIST_MONTH_END
Last trading day
The last session in the instrument's local month, with collection windows excluded
None
Cut-off
The session close plus 10 minutes
REPORTING_SHAREHOLDER_LIST_MONTH_END_GRACE
Timezone
The instrument's or venue's calendar timezone, else Asia/Dubai
REPORTING_SHAREHOLDER_LIST_FALLBACK_TIMEZONE
Correction after a bust
On, automatic
REPORTING_SHAREHOLDER_LIST_CORRECTIONS
Correction lookback
7 days
REPORTING_SHAREHOLDER_LIST_CORRECTION_LOOKBACK
Ownership basis
On-chain register
None

Set a boolean setting to false to switch the trigger off. The auction trigger has no setting and is always on. The grace period and the lookback take durations. The timezone takes an IANA name such as Asia/Dubai. When you switch the month-end trigger off, run it by hand as the Choose when to run it table describes. Corrections have no manual equivalent.

Corrections

A bust can reverse a trade that an earlier list counted. When it does, the platform opens a correction delivery on its own. You do not post it.

  • The correction supersedes the newest delivered list that counted the trade. It is recorded at the latest final block and carries the reason bust.
  • Only a bust inside the lookback window is considered, and only a trade settled at or before the list's record block and busted after it. The correction waits until the final block is newer than the bust.
  • The earlier edition stays downloadable. Its row shows Superseded and names the list that replaces it. The correction row shows “Correction”, “Reason: trade bust”, and the block it replaces.
  • In the API, supersedesId on the correction and supersededById on the earlier list point at each other. correctionReason is bust.
  • A newer bust against a correction supersedes that correction in turn. One correction exists per superseded list.
  • The issuer's owners get the usual delivery email. It names the corrected record block and the reason, and carries no holder data.

When REPORTING_SHAREHOLDER_LIST_CORRECTIONS is false, the platform opens no correction, and no manual step creates one. A list you schedule is an ordinary scheduled list: it does not carry the correction trigger or the reason bust, and it does not mark the earlier list as superseded. If you still want the issuer to have a list after a bust, schedule one at the newest final block that is later than the bust, and tell the issuer which list replaces which.

Ownership basis

The list is the on-chain register: every wallet that holds the security at the record block. Each delivery is stamped with ownershipBasis on-chain-register, and the PDF prints it under the record block. The summary of an on-demand read carries the same field. A custodian's shared wallet appears as one holder under the custodian's wallet. The list does not look through to the custodian's investors, so the issuer needs the custodian's own records for those.

Schedule from the Console

  1. Open the security, go to “Holders” and select “Export” › Holder register….
  2. Choose Block and enter the record block, or keep the newest final block.
  3. Select Deliver to issuer. The sheet moves to “Deliveries”.

“Export” › Issuer deliveries… opens the same tab. The table shows each delivery with its record block, trigger, status, delivered time, holder count and downloads. The trigger reads “After auction”, “Requested”, “Month end” or “Correction”. “Requested” is a list an operator asked for by hand, which the API calls the scheduled trigger. A pending row reads “Waiting” and refreshes while the pass produces it. A delivered row offers one download per format. A refused row names the reason.

Schedule with the Platform API

Post the record block as a string:

curl -X POST "https://your-instance.example.com/api/v2/reporting/shareholder-lists/<tokenAddress>/deliveries" \
  -H "X-Api-Key: <your api key>" \
  -H "Content-Type: application/json" \
  -d '{"recordBlock": "1200"}'

The platform answers with the delivery. A block past the newest final block stays pending until the block is final.

A security your organisation does not hold answers 404 with code reporting.register.SHAREHOLDER_LIST_NOT_FOUND. That answer covers a missing security, a security of another organisation, and a security with no resolvable issuer. Your organisation learns nothing about other organisations from it.

List deliveries

curl "https://your-instance.example.com/api/v2/reporting/shareholder-lists/<tokenAddress>/deliveries?offset=0&limit=50" \
  -H "X-Api-Key: <your api key>"

The newest record block comes first. limit takes 1 to 200 and defaults to 50, and a larger value is refused as invalid. meta.total counts every delivery of the security in your organisation, meta.canSchedule says whether you may schedule one, and links pages through the list:

{
  "data": [
    {
      "id": "0199b2d1-8a19-7a08-8d21-4d33e3c1a111",
      "chainId": 1,
      "tokenAddress": "0x0000000000000000000000000000000000000001",
      "recordBlock": "900",
      "trigger": "auction-settled",
      "ownershipBasis": "on-chain-register",
      "supersedesId": null,
      "supersededById": null,
      "tradingDay": null,
      "correctionReason": null,
      "status": "delivered",
      "requestedBy": null,
      "attempts": 1,
      "lastError": null,
      "createdAt": "2026-10-10T08:05:00.000Z",
      "settledAt": "2026-10-10T08:10:00.000Z",
      "holderCount": 12,
      "refusal": null,
      "notice": "sent",
      "noticeAttempts": 1,
      "parts": {
        "csv": {
          "objectKey": "shareholder-lists/org_a/0199b2d1-8a19-7a08-8d21-4d33e3c1a111.csv",
          "digest": "9f2c4a7e1b5d83f06a2c9e4b7d105f3a8c6e2d4b9f105a3c7d2e6b8a1f4c5d6",
          "contentLength": 1840
        },
        "xlsx": {
          "objectKey": "shareholder-lists/org_a/0199b2d1-8a19-7a08-8d21-4d33e3c1a111.xlsx",
          "digest": "41bd9c0e77f2a5d18c3e6b0a4f9d2c7e5b8a1f306d4c5e9b8a7f1c2d3e4a5b6c",
          "contentLength": 6231
        },
        "pdf": {
          "objectKey": "shareholder-lists/org_a/0199b2d1-8a19-7a08-8d21-4d33e3c1a111.pdf",
          "digest": "c7e30bd4a19f6c2d5e8b40a173f5c9d6e2b80417f6a3c5e9d0b2f486a1c7d3e5",
          "contentLength": 12904
        }
      }
    }
  ],
  "meta": { "total": 1, "canSchedule": true },
  "links": {
    "self": "/api/v2/reporting/shareholder-lists/0x0000000000000000000000000000000000000001/deliveries?offset=0&limit=50",
    "first": "/api/v2/reporting/shareholder-lists/0x0000000000000000000000000000000000000001/deliveries?offset=0&limit=50",
    "prev": null,
    "next": null,
    "last": "/api/v2/reporting/shareholder-lists/0x0000000000000000000000000000000000000001/deliveries?offset=0&limit=50"
  }
}

Download a stored edition

curl "https://your-instance.example.com/api/v2/reporting/shareholder-lists/<tokenAddress>/deliveries/<deliveryId>/download?format=csv" \
  -H "X-Api-Key: <your api key>" \
  -o shareholder-list-900.csv

format is required: csv, xlsx or pdf. The platform returns the stored bytes. A download of a pending delivery answers 503, as does a file whose bytes no longer match the recorded digest or length. A refused delivery never becomes available: a security-not-found or not-issuer refusal answers 404, a feature or history refusal answers 422 with code reporting.register.SHAREHOLDER_LIST_REFUSED, a file-unwritable refusal answers 500 with code reporting.artifact.UNWRITABLE (DALP-14085), a storage-refused refusal answers 500 with code reporting.dependency.REFUSED (DALP-14088), and a chain-read-invalid refusal answers 500 with code core.network.CHAIN_READ_INVALID (DALP-14108). None of these 500 answers is retryable. A delivery of another organisation answers 404.

How the issuer learns that the list arrived

The platform emails every owner of the issuing organisation when a delivery is delivered or refused. The mail names the security, the record block and the trigger, states the reason when refused, and links to the security page in the Console. It carries no holder data.

A notice that fails to send retries on later passes after a backoff, up to three attempts, then stays failed. The wait is 10 minutes after the first failure and 20 minutes after the second. A retry only emails the owners who have not received that notice. Recipients who already received it are not emailed again. When mail is not configured, the notice stays not-configured. If the platform crashes after the mail transport accepts a notice but before that acceptance is recorded, the next pass may email that recipient again, so a recipient can receive the same notice twice.

Audit and retry

Each delivered delivery, refused delivery, sent notice and failed notice writes an operations audit row for the issuing organisation. You read those rows through the reporting audit trail. The audit trail also records who made the schedule call and each download.

A short-lived failure leaves the delivery pending with an attempt count and a last error, and a later pass retries it after a backoff. The wait starts at 5 minutes and doubles with each attempt, up to 24 hours. An unreconciled list waits the same way: it stays pending with an attempt and a last error, and a later pass delivers it once the holders add up to total supply. A failure that gives the same result on every attempt does not wait: a list that cannot be written in one of its formats is refused as file-unwritable on the first attempt. A refused delivery is terminal and never retries.

A delivery whose stored record is damaged is skipped and counted, and the pass carries on with the other deliveries. The damaged delivery is neither produced nor emailed, and a read that needs it answers temporarily unavailable rather than returning partial data. An audit event whose stored record is damaged is skipped the same way and is missing from the reporting audit trail, while the delivery's other audit events are still recorded. Contact support with the security and the record block.

When a delivery is refused

refusal
What it means
What to do
feature-not-attached
The security had no historical-balances feature at the record block.
See the matching row under the on-demand refusals.
feature-detached
The historical-balances feature was detached at or before the record block.
Choose a record block before the detachment block.
enablement-after-record-block
The feature was attached after the record block.
Choose a record block at or after the attachment block.
history-incomplete
The security had supply before the feature was attached.
No record block can be delivered.
security-not-found
The security was missing at the record block.
Check the address and the block.
not-issuer
The organisation no longer issues the security.
No block can be delivered to it.
file-unwritable
One of the files could not be written, for example because the holders exceed what one XLSX worksheet holds.
Download the list on demand in another format, such as CSV, or contact support.
storage-refused
File storage refused the edition's files, so they were never stored.
Contact support, then schedule a new delivery.
chain-read-invalid
The chain reverted or answered unusable data when the record block's time was read.
Contact support with the security and the record block.

A block past the newest final block never refuses a delivery, and neither does an unreconciled list. Both wait as pending: the delivery for a block past the newest final block until that block is final, and the delivery for an unreconciled list until the projection reconciles.

Systems that organisations share

The platform names the issuing organisation from the system the security lives in. When one organisation holds the system, that organisation is the issuer. When organisations share the system, the recorded issuer is the issuer only while it still holds the system. A missing recorded issuer, or one that no longer holds the system, names no issuer: the security gets no auction delivery, and scheduling answers not found.

What the file contains

The CSV and XLSX files have one row per wallet that holds the security after the record block, largest holding first. A wallet with a zero balance is left out. The PDF shows the same holders with the network and its chain ID, the record block and its time, and the reconciliation result. Its holders table ends with a totals row: the number of holders, the units they hold, and the share of supply those units are.

Where the chain records a field, the column maps to its field in the shareholder response of Commission Implementing Regulation (EU) 2018/1212, Annex Table 2, and to its entry in the register of members under section 118 of the ADGM Companies Regulations 2020. The file alone is not a complete shareholder response or register of members: it carries the contract address in place of an ISIN and no names or addresses, which the issuer completes from its own records.

Column
Content
Table 2 field
Section 118 entry
security
The security's contract address, in place of an ISIN.
A.4
Shares held, by class
symbol
The security's symbol.
recordDate
The date of the record block, as YYYYMMDD in UTC.
A.5
recordBlock
The record block.
account
The holder's wallet, the account the shares are held in.
B.7
identity
The holder's registered OnchainID identity. Empty when the wallet has no registered identity.
C.1
Member
country
The holder's registered country as an ISO 3166-1 alpha-2 code.
C.6
shares
The holder's balance in the security's own units.
C.11
Shares held
percentOfSupply
The holder's share of total supply, cut to four decimal places and not rounded, so the column never adds up to more than 100.

In the XLSX file, recordDate and percentOfSupply are numbers. recordBlock is a number when it fits spreadsheet precision exactly, and text otherwise, so the block identifier cannot change. shares is a number only when every holder's balance and their sum fit a spreadsheet number exactly, which is 15 significant digits. Otherwise the whole column is text, so that no amount is rounded. A spreadsheet total of a text column is 0, so check the column type before you add it up. The CSV file holds text in every column.

The file does not hold names or postal addresses. The issuer matches each identity to the member's name and address in its own records.

When DALP cannot produce the list

A refused list returns DALP-0753 with a reason in data.reason:

data.reason
What it means
What to do
feature-not-attached
The security had no historical-balances feature at the record block, so there is no balance history to list.
No past block can be listed for a security created without the feature. The Console attaches the feature only when it creates a security. A feature attached later through the Platform API does not help a security that already had supply, because its listing is then refused with history-incomplete. Issue new securities with the feature. For a security whose feature was detached, choose a record block from before the detachment.
feature-detached
The feature was detached at data.boundaryBlock. The record block is at or after it.
Choose a record block before data.boundaryBlock.
enablement-after-record-block
The feature was attached after the record block. data.boundaryBlock is the block it was attached at.
Choose a record block at or after data.boundaryBlock.
history-incomplete
The security had supply before the feature was attached, so the balance history is incomplete. data.boundaryBlock is the block the feature was attached at.
No record block can be listed.
record-block-not-final
The record block is later than the newest final block, data.boundaryBlock.
Wait until the block is final, or leave the record block out.

Was this page helpful?