Skip to main content
Partner-managed investor onboarding is the default in the Zest Public API: you run KYC on your side and POST the resulting investor records to Zest. Zest persists each row as a Person and returns a stable zestPersonId for use in subscription flows.

Bulk import endpoint

The endpoint accepts an array of individual-investor records. Institutional/entity investors are not supported on this endpoint in v1 — those will surface in a follow-up endpoint.

Request body

On success, the response returns zestPersonId per row. Use it on subsequent POST /v1/spvs/{slug}/subscriptions, POST .../forms, and POST .../fundings calls to identify the investor. If you ever see 404 not_found on a subscription create for a zestPersonId returned by POST /v1/investors, surface the errorId to Zest support — never retry the investor create as a workaround.

Field reference

Control plane

Personal

Contact

residentialAddress (nested, required)

mailingAddress (nested, required)

Used for tax + subscription document delivery. Often identical to residentialAddress but separated for jurisdictions where they differ.

Tax

Compliance

Identification

Required-nullable convention

Three fields (hasBackupWithholding, qualifiedStatus, passportNumber) are required-nullable: the key MUST appear in your JSON body, but the value MAY be null. Omitting the key entirely returns 400 validation_error; sending null is valid.

Qualified status

qualifiedStatus accepts one of the following enum values, or null:
  • QUALIFIED_PURCHASER
  • QUALIFIED_CLIENT
  • TRUSTEE_QUALIFIED_PURCHASER
  • KNOWLEDGEABLE_EMPLOYEE
  • KNOWLEDGEABLE_EXECUTIVE
Any other string value returns 400 validation_error at the request level.

Partial-success semantics

The endpoint always returns 200 OK with a per-row result envelope. A single bad row never fails the batch (with the exception of request-level validation errors — see Failure cases below).

Per-row error codes

Correlation: partnerInvestorId

partnerInvestorId is the stable join key between your system and Zest. It must be:
  • Unique within your tenant.
  • Present on every row (no nulls).
  • Stable across retries.
Zest persists the (client_id, partnerInvestorId) -> zestPersonId mapping. Replaying the same id always returns the existing zestPersonId, never a fresh one. This makes the endpoint safe to retry under any failure mode — duplicates are deduplicated.

Idempotency layers

There are two independent layers of replay protection:
  1. Row-level dedup via partnerInvestorId (always on).
  2. Response caching via Idempotency-Key header (24h TTL, optional).
For long-running bulk imports, layer (1) is sufficient — even if your client crashes mid-batch and you retry from scratch, all created rows return already_exists on the second pass. For interactive single-row creates, add Idempotency-Key so the cached response is served byte-for-byte on retry.

Webhook fan-out

Each created row fires one investor.created webhook with a minimal payload — only the six fields needed for correlation:
Sensitive fields you sent in the create (such as taxId, passportNumber, addresses) do not appear in the webhook payload — they stay inside Zest. If you need the full investor record later, request a fetch endpoint (currently a deferred follow-up; raise with Zest support). already_exists and failed rows do not fire webhooks — replays are silent on the wire.

Failure cases