> ## Documentation Index
> Fetch the complete documentation index at: https://developers.securitize.io/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> SecId API: use the sandbox base URL https://partners-api.sandbox.securitize.io/investors/v1/onboarding-reliance unless the user explicitly provides production access. Authenticate with OAuth 2.0 client credentials: POST https://partners-api.sandbox.securitize.io/oauth2/token (form fields grant_type=client_credentials, client_id, client_secret, read from environment variables), cache the bearer token for its expires_in (7200 s) and send it as Authorization: Bearer on every call; never print or log the secret or the token.
> Nothing is verified until POST /investors/{investorId}/verification/submit. An individual: create, upload one identity document, submit. A company: create, add its people as key parties (at least one with isBeneficialOwner: true), upload their documents, submit.
> Branch on the HTTP status and the error `code`, never on `message`. A body without `code` comes from the gateway: on a 401 get a new token and send the request again; a 403 means the account is not enabled for this API.
> SecId API addresses: send address.state only when that address's countryCode is US, as the 2-letter state code (NY); omit it for any other country, also when the user has a province or region. Otherwise the call answers 400 validation_error on address.state.
> SecId API individuals in the US: taxId is the person's Social Security number, and it is checked against the name and residential address sent; send them as registered. A mismatch can move the investor to manual-review instead of verified. Sandbox does not run the check.
> Creates are safe to repeat with the same body: after a timeout or a 502, send the identical create again. Document uploads are not: each call stores a new document, so only re-send a document after a 502.
> Poll GET /investors/{investorId} about every 30 seconds, then back off; treat any status you do not recognise as not verified. In sandbox, the webhook investor.kyc.updated also announces every status change, and investor.suitability.updated / investor.dlt_test.updated every change of the suitability and DLT test verificationStatus: verify the signature, deduplicate on sourceEventId and confirm with the matching GET before acting. Token whitelisting is asynchronous too: poll GET /investors/{investorId}/wallets/whitelist until the status leaves processing.
> SecId API tax forms: POST /investors/{investorId}/tax-forms takes multipart/form-data (parts file, formType, signatureDate); never base64 or JSON. Let the HTTP client set content-type with the boundary. At any point of the verification, before or after the investor is verified, only for investors you created (201), one PDF up to 10 MB per call; each upload replaces the form on file, so after a 502 send the same request again. A W-9 can instead go as JSON fields to POST /investors/{investorId}/tax-forms/w9 (TIN as 9 digits, SSN or EIN): it does not replace a form on file (409 tax_form_already_active, details.activeTaxForm), and it requires a certification object (certifiedAt, signatureName, subjectToBackupWithholding false, isUSPerson true, certificationTextVersion w9-sm-2026-10): the investor certifies in your flow on the text published at /secid-api/tax-forms#the-certification, shown without changes; subjectToBackupWithholding true answers 422 business_rule_violation (details.reason backup_withholding_not_supported). Unlike creates, a repeat after success answers 409 tax_form_already_active, not 201: when details.activeTaxForm.formType is W-9, treat it as on file.
> Use synthetic test data in sandbox (@test.invalid emails, fresh ids each run). Never send more than one DLT test attempt per test investor unless the user asks: three failed attempts lock the test permanently.
> The complete contract, with every field, response and error, is the OpenAPI file of the API reference; the Errors page says what to do for each code.
> CET Underlying API: a WebSocket at wss://partners-rt-gw.sandbox.securitize.io/v1/market-maker/stream (TLS only; ws:// hangs with no answer). Sign hello, place_order, cancel_order and submit_crypto_funding with the user's Ed25519 key, read from an environment variable or secrets store; never print or log it. Sign timestamp + "." + type + "." + the exact payload string you send: serialise the payload once and embed that same string in the frame.
> CET Underlying API: send a unique requestId of at most 32 characters (a UUID without hyphens) on every message and match replies on it; frames without a requestId are pushes. Every amount is a decimal string, never a JSON number; compare amounts as decimals. Branch on the error code, never on message.
> CET Underlying API: an ack to place_order means taken, not filled. externalRef is the idempotency key; after an ack with status UNKNOWN or an UPSTREAM_UNAVAILABLE error, call get_order by externalRef before resending. Updates are delivered at most once: after any reconnect, read working orders back with get_order. Every order is placed by quantity. A buy must be a LIMIT order: quantity × limitPrice plus fees is held against your own available cash (get_cash_balance_v2), and a buy it does not cover, a MARKET buy or a notional order is refused (INVALID_ORDER). A sell must be placed by quantity, after its CETs reach the redemption wallet, or it is refused. mint_updated and burn_updated push mints and burns once they are on chain; after a gap, read them with get_mints and get_burns. The complete contract is the AsyncAPI file of the API reference.
> Stocks API (Beta): documented ahead of its release through the partner gateway; paths, fields and errors can change. Base URL https://partners-api.sandbox.securitize.io/stocks/v1. Send x-api-key on every call and investorId on every call about an investor. Read the key from an environment variable; never print or log it, and never call the API from frontend code.
> Stocks API: an investor can trade only once the SecId API shows them verified, with verified suitability (and a passed DLT test in the EEA), and once their wallet on the stock's chain is linked with their signature of the agreement message, signed verbatim. There is no whitelisting call: the first trade whitelists the wallet.
> Stocks API: orders are fill or kill. GET /stocks/realtime-price, prepare within about 20 seconds (signedPrice), have the investor's wallet sign the returned transaction unchanged and sign-only (never send it), then submit it once with POST /stocks/execute-signed-transaction within 120 seconds. Amounts are raw base units as strings; USDC has 6 decimals. The outcome is on chain: follow transactionHash.
> Stocks API: production access requires a UI audit by Securitize Compliance of the live screens: the disclosures and the market data shown, the NBBO included.
> Corporate Actions API (Beta, 0.x): in development and subject to change; paths, fields, enumerations and event names can change before 1.0.0. Base URL https://partners-api.sandbox.securitize.io/corporate-actions/v1. Get a bearer token with OAuth 2.0 client credentials at POST https://partners-api.sandbox.securitize.io/oauth2/token (no scope, 2 hours), cache it, and send Authorization: Bearer on every call. Read the client id, secret and the webhook signing secret from environment variables; never print or log them, and never call the API from frontend code.
> Corporate Actions API: webhooks arrive signed with the Standard Webhooks scheme under the svix-id, svix-timestamp and svix-signature headers (HMAC-SHA256 over id.timestamp.raw body, base64, v1 prefix; the secret is base64 after whsec_). Verify before parsing, reject a timestamp more than 5 minutes off, answer 2xx within 10 seconds. Delivery is at least once and can be out of order: deduplicate on sourceEventId, keep the highest version per object (objectId), order by sequence per stream.id, and replace the object with data (never merge).
> Corporate Actions API: treat an unknown caType, reasonCode, eventType or enum value as informational, never as an error. A restriction ends only with ca.restriction.removed; expectedEnd is informational. marketStatus UNAVAILABLE means do not trade; the reason field carries a restriction reasonCode or a market reason such as PRE_MARKET. GET /tokens?status= filters by marketStatus (OPEN, CLOSED, UNAVAILABLE), not by the token's ACTIVE status.
> Corporate Actions API: the event routes are at https://partners-api.sandbox.securitize.io/webhooks/v1 (not under /corporate-actions/v1) and cover every webhook family sent to the partner; filter with eventTypes=ca.* for corporate actions only. After an outage call POST /events/recover with since at the start of the outage (resends failed deliveries only), or page GET /events?since= (newest first, iterator paging, 90 days), then read state with GET /tokens/{tokenId}/status and GET /corporate-actions/{sourceCaId}. In sandbox, POST /sandbox/events with a scenario and one of your tokenIds publishes a scripted sequence at once; order the events by sequence, not by arrival.
> Investor Activity API: base URL https://partners-api.sandbox.securitize.io/v1. Authentication is OAuth 2.0 client credentials only (no API key): POST https://partners-api.sandbox.securitize.io/oauth2/token (no scope, 2 hours), cache the token, and send Authorization: Bearer on every call. The token identifies the investor account; there is no investor id to pass. Read the client id, secret and the webhook signing secret from environment variables; never print or log them, and never call the API from frontend code.
> Investor Activity API: every amount is an integer string in base units with a sibling decimals (human = amount / 10^decimals); parse with BigInt or a decimal type, never a float. Cash amounts (deposits, confirmations, payouts) are in the minor units of currencyId (1 = USD, decimals 2); token amounts (transactions, mintings, holdings) in the token's base units. Never add cash to tokens.
> Investor Activity API: reconcile a subscription by grouping GET /deposits?tokenId= on tokenIssuanceId and matching the transaction in GET /transactions?tokenId= with the same tokenIssuanceId (null on a deposit means not issued yet; null on a transaction means not an issuance). Each issuance has a deposit movement (cash in) and a liquidation movement (that cash applied to the tokens) for the same amount: count the cash once, by direction=deposit, never by adding every movement. Page with offset and limit (max 100) until offset + meta.length reaches meta.total; there is no tokenIssuanceId filter. Reconcile a redemption by txHash (transfer, investor.redemption.pending, payout sourceTransaction) and by the redemption's data.id across pending, confirmed, payout.initiated and payout.completed.
> Investor Activity API: webhooks arrive signed with the Standard Webhooks scheme under the svix-id, svix-timestamp and svix-signature headers (HMAC-SHA256 over id.timestamp.raw body, base64, v1 prefix; the secret is base64 after whsec_). Verify before parsing, reject a timestamp more than 5 minutes off, answer 2xx within 15 seconds. The body is {eventType, eventId, timestamp, data}: deduplicate on eventId (not svix-id), upsert by data.id, never move a record back to an earlier step, and treat an unknown eventType, processType or paymentMethod as informational. In webhooks a field with no value is absent; in endpoint answers it is null. After an outage page GET /webhook-events (newest first, iterator paging, optional eventType) back to the start of the outage.
> SecId hosted onboarding: instead of the API, a partner can send an investor to https://stocks.securitize.io/partners?tokenId=<stock token id>&partnerUrl=<return URL> (tokenAddress may replace tokenId; partnerUrl must be an absolute https URL, URL-encoded). Securitize runs identity verification, the investment profile and the wallet there. Nothing is appended to partnerUrl on return: check the wallet on chain before trading (Beta on Solana).

# Changelog

> Changes to the SecId API and these docs, newest first.

**November 2026**

Hosted onboarding

* **Hosted onboarding is documented.** Instead of building onboarding with the API, you can link investors to a page Securitize hosts with only the onboarding steps: identity or business verification, the investment profile and the wallet. The investor comes back to your `partnerUrl` when done. See [Hosted onboarding](https://developers.securitize.io/secid-api/hosted-onboarding.md).

**November 2026**

PEP answer optional in the compact suitability

* **`isPoliticallyExposedPerson` is optional in `POST /investors/{investorId}/suitability/compact`.** A compact suitability without it is complete and gets the same verdict. When sent, it is stored and `GET /investors/{investorId}/suitability/compact` returns it; when not, `answers` leaves it out. The full suitability still requires it. See [The compact suitability](https://developers.securitize.io/secid-api/suitability-and-dlt-test.md#the-compact-suitability).

**November 2026**

Tax identifier check for US persons

* **A US person's `taxId` is checked against their name and address.** For an individual with a US address and `taxCountry` `US`, verification also checks that the Social Security number in `taxId` belongs to the name and residential address sent. Where investors become `verified` after screening, the status stays `processing` until the check answers, and a mismatch moves the investor to `manual-review`. Where they are `verified` on submission, the status does not change. Sandbox does not run the check. See [Statuses](https://developers.securitize.io/secid-api/statuses.md#investor-status).

**November 2026**

US state codes in addresses

* **Breaking: `address.state` is a US state code.** On every create and correction of an individual, a company and a key party, `address.state` takes the 2-letter code of a US state or `DC` when that address's `countryCode` is `US`, and must be omitted for any other country. A state on an address outside the US, or a US value that is not a state code (`New York`), answers `400 validation_error` on `address.state` and stores nothing; both were accepted before. `null` and an empty string read as omitted, and codes are case-insensitive and stored in upper case. See [Create the person](https://developers.securitize.io/secid-api/onboard-an-individual.md#1-create-the-person).

**November 2026**

Documents as multipart, files up to 20 MB

* **Both document routes also take `multipart/form-data`, with files of up to 20 MB each.** `POST /investors/{investorId}/documents` and `POST /investors/{investorId}/key-parties/{keyPartyId}/documents` accept each file as a part of its own: `front`, plus `back` for a two-sided identity document, or `file` for a company document, with `type`, `documentNumber` and `expiresAt` as text parts. The file's type is read from its first bytes. The rules and the answers are those of the JSON body, and JSON uploads are unchanged, with their 10 MB limit. A file part over 20 MB answers `413 payload_too_large`; a part that does not belong to the document, or an empty one, answers `400 invalid_document`. See [Two ways to send the files](https://developers.securitize.io/secid-api/documents.md#two-ways-to-send-the-files).
* `415 mime_not_supported` on the document routes now reads `Content-Type must be application/json or multipart/form-data (received: …)`.

**November 2026**

Document files up to 10 MB

* **Document files can be up to 10 MB each**, measured on the file itself before base64 (previously 5 MB). This applies to identity documents and company documents alike. A two-sided document travels with both files at the maximum in one call: the request body limit is now about 31 MB. A file over 10 MB still answers `413 payload_too_large` and nothing is stored.

**October 2026**

Suitability verdicts and the compact suitability

* **`POST /investors/{investorId}/suitability` decides an individual's verdict from the answers.** It answers `verified`, `rejected` (the investment approach is not aggressive growth, the investor could bear a loss of up to 30% only, or does not accept illiquidity) or `processing` (a licensed securities professional or someone affiliated with a securities firm, reviewed by Securitize's compliance team). A `verified` suitability also approves the investor's Securitize Markets brokerage account once their `status` is `verified`. `acceptsIlliquidity: false` now rejects an individual's suitability. An entity's suitability still reads `verified` at once. See [The verdict](https://developers.securitize.io/secid-api/suitability-and-dlt-test.md#the-verdict).
* **New operations: `POST` and `GET /investors/{investorId}/suitability/compact`.** A shorter suitability for individuals, the part a Securitize Markets brokerage account for stocks and '40 Act funds requires, with a subset of the full suitability's fields and the same verdicts. An entity answers `422` with `details.reason: investor_type_not_supported`. See [The compact suitability](https://developers.securitize.io/secid-api/suitability-and-dlt-test.md#the-compact-suitability).
* Both answer `422 business_rule_violation` with `details.reason` `suitability_under_review` or `suitability_not_approved` while the verdict is `processing` or `rejected` and the full suitability is not complete. Nothing is stored; the [suitability webhook](https://developers.securitize.io/secid-api/suitability-webhooks.md) announces the next decision.

**October 2026**

W-9 certification, tax forms before verification

* **`POST /investors/{investorId}/tax-forms/w9` requires `certification`.** The investor certifies the W-9 in your flow on the certification text Securitize publishes, and you send what they agreed to: `certifiedAt`, `signatureName`, `subjectToBackupWithholding`, `isUSPerson` (`true`) and `certificationTextVersion` (`w9-sm-2026-10`). A W-9 without it, or with an invalid one, answers `400 validation_error`. See [The certification](https://developers.securitize.io/secid-api/tax-forms.md#the-certification).
* A W-9 subject to backup withholding cannot be sent as data: `subjectToBackupWithholding: true` answers `422 business_rule_violation`, with `details.reason` `backup_withholding_not_supported`.
* **Tax forms no longer wait for the investor to be `verified`.** `POST /investors/{investorId}/tax-forms` and `POST /investors/{investorId}/tax-forms/w9` accept the form at any point of the verification; neither answers `422 investor_not_verified` any more.

**October 2026**

OAuth 2.0 authentication

* **Authentication is OAuth 2.0 client credentials.** Exchange the client id and secret Securitize issues you for a bearer token at `POST https://partners-api.sandbox.securitize.io/oauth2/token`, and send it as `Authorization: Bearer` on every request; it lasts 2 hours. Ask your Securitize contact for client credentials. See [Sandbox and authentication](https://developers.securitize.io/secid-api/sandbox-and-auth.md).
* The gateway answers a missing token with `401 invalid_request` and an unknown or expired one with `401 invalid_token`, in the OAuth shape `{"error", "error_description"}`. See [When the token is wrong](https://developers.securitize.io/secid-api/sandbox-and-auth.md#when-the-token-is-wrong).
* The same token reads your webhooks back at `/webhooks/v1/events` ([Replay and recover](https://developers.securitize.io/webhook-events.md)).
* The token request is in the API reference: [Get an access token](https://developers.securitize.io/api-reference/authentication/get-an-access-token.md) returns a sandbox token to try the other operations with.

**October 2026**

Suitability webhooks

* **New webhooks `investor.suitability.updated` and `investor.dlt_test.updated`, in sandbox.** Securitize POSTs them to your endpoint every time the `verificationStatus` of an investor's suitability or DLT test changes, whoever changed it, with your `investorId`, your `externalInvestorId` and the new verdict. See [Suitability webhooks](https://developers.securitize.io/secid-api/suitability-webhooks.md). An endpoint without an event-type filter starts receiving them: answer `2xx` to types you do not handle.

**October 2026**

Whitelisting read on chain

* **`GET /investors/{investorId}/wallets/whitelist` reads the tokens' on-chain registries.** After your own whitelistings, it lists every other token enabled for your account whose registry already holds one of your wallets for the investor, as `whitelisted`: tokens that share a registry share their whitelistings. See [Tokens that share a registry](https://developers.securitize.io/secid-api/whitelist-a-wallet.md#tokens-that-share-a-registry).
* `whitelistedAt` is optional: a row is listed without it when you never whitelisted that wallet on a token of the same registry.
* A whitelisting reads `whitelisted` as soon as the registry holds the wallet, in the list and in the answer of `POST /investors/{investorId}/wallets/whitelist`.

**October 2026**

W-9 as data

* **New operation: `POST /investors/{investorId}/tax-forms/w9`** sends the fields of a verified investor's W-9 as JSON, instead of a signed PDF; Securitize generates the form's PDF and puts it on file, recorded as sent by you. Only the partner that leads the investor's verification can send it. You collect the investor's certification in your own flow. See [Send a W-9 as data](https://developers.securitize.io/secid-api/tax-forms.md#send-a-w-9-as-data).
* A W-9 sent as data does not replace a form on file: new error code `409 tax_form_already_active`, with `details.activeTaxForm`. To correct or replace the form, contact Securitize support.

**October 2026**

Read an investor by its investorId

* **`GET /investors/{investorId}` takes the `investorId` Securitize returns**, like every other path that names an investor. It replaces `GET /investors/{externalInvestorId}`; your own id in that path answers `404 investor_not_found`. To find an investor by your `externalInvestorId`, filter the list: `GET /investors?externalInvestorId=…`. The response, `include` and the access rules are unchanged.

**October 2026**

Tax forms

* **New operation: `POST /investors/{investorId}/tax-forms`** sends the tax form a verified investor signed (`W-9`, `W-8BEN` or `W-8BEN-E`), as one PDF of up to 10 MB, so Securitize has it on file for withholding. The body is `multipart/form-data`: the PDF in the `file` part, with `formType` and `signatureDate`. Only the partner that leads the investor's verification can send it, and each upload replaces the form on file. See [Send a tax form](https://developers.securitize.io/secid-api/tax-forms.md).
* New error codes: `422 tax_form_type_not_allowed`, with `details.allowed`, and `502 tax_form_registration_failed`.

**October 2026**

KYC webhooks

* **New webhook `investor.kyc.updated`, in sandbox.** Securitize POSTs it to your endpoint every time the `status` of one of your investors changes, with your `investorId`, your `externalInvestorId` and the new status. See [KYC webhooks](https://developers.securitize.io/secid-api/kyc-webhooks.md); your Securitize contact registers the endpoint and hands you its signing secret.

**October 2026**

Larger document files

* **Document files can be up to 5 MB each**, measured on the file itself before base64 (previously about 1.5 MB). This applies to identity documents and company documents alike. A two-sided document travels with both files at the maximum in one call: the request body limit is now about 16 MB. A file over 5 MB still answers `413 payload_too_large` and nothing is stored.

**October 2026**

Developer portal

The SecId API documentation moves to this portal: the API reference, the guides for individuals, companies, wallets, whitelisting, suitability and the DLT test, and the material coding agents read.

Changes since the integration manual v1.1 (September 2026):

* **An individual's verification starts with the submit.** `POST /investors/{investorId}/verification/submit` is now a required step for individuals, as for companies. The document that completes the set answers `verification.status: awaiting_submission` and the investor stays `none` until you submit. Corrections are accepted until the submit.
* **Creates are repeatable with the same body.** The same create sent again answers `201` with the same id; a different body for the same `externalInvestorId` answers `409`. This applies to investors and key parties.
* **The batch create with documents is removed.** `POST /investors` no longer exists; create the investor, then upload its documents one by one.
* **`taxId` and `taxCountry` (individuals) and `entityIdNumber` (companies) are required**, and placeholder values are refused.
* **Wallets are identified by their address alone**; `title` is no longer accepted. Registering an address the investor already holds answers `201`, and your list shows the wallets you registered.
* **New operations**: token whitelisting (`/investors/{investorId}/wallets/whitelist`), the suitability and the DLT test.
* **A date of birth after today is refused** with `400 validation_error`, on individuals and person key parties.
* **A document's `content` can also be a data URL** (`data:<mime>;base64,…`), as a browser's `FileReader` produces it.
* Error messages are clearer and never name internal systems. Branch on `code`, as always. A method and path that match no operation answer `404 route_not_found`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.