Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.zestequity.com/llms.txt

Use this file to discover all available pages before exploring further.

A subscription is a single investor’s commitment to deploy capital into a single SPV at a specified share class. Partners create subscriptions in bulk per SPV via POST /v1/spvs/{slug}/subscriptions.

Internal model

Internally, every subscription auto-creates a Bid row with source=partner-api. There is no separate “Subscription” collection on the Zest side — the partner-visible projection is curated from the Bid. This matters because:
  • Zest’s internal investment workflow already operates on Bids; partner-sourced subscriptions inherit that machinery for free.
  • Admin-side actions (e.g. mark-as-completed) are Bid actions that emit subscription-shaped webhooks (subscription.completed).
You never need to know the underlying Bid id; the partner-visible subscriptionSlug is stable across the lifecycle.

Server-side projections

Each row in a successful POST /v1/spvs/{slug}/subscriptions call produces three internal rows as part of the same request:
RowVisible to partner?Purpose
Bid (source = partner-api)Yes (as the subscription).Drives the existing internal investment workflow.
Shadow OpportunityBidder (status: partner-managed)No.Lets the existing Zest admin UI list partner-sourced subscriptions alongside Zest-native bidders for the same Opportunity. Read-only; partner actions still flow through the v1 endpoints.
Stored payload metadata on the Bid (partner ids, share class)Echoed in webhooks.Lets webhook envelopes carry partnerSubscriptionId / partnerInvestorId without an extra lookup.

Lifecycle

   POST /v1/spvs/{slug}/subscriptions


   ┌──────────────────────┐
   │  status: "pending"   │  ◄── subscription.created webhook fires
   └──────────┬───────────┘

              │  POST .../forms (signed PDF / image)

   ┌──────────────────────────┐
   │  signed form on record   │  ◄── signed_subscription_form.uploaded
   └──────────┬───────────────┘

              │  POST .../fundings (wire-receipt + amount/currency/wireRef)

   ┌──────────────────────────────┐
   │  funding receipt on record   │  ◄── funding_receipt.uploaded
   └──────────┬───────────────────┘

              │  Zest admin marks Bid Completed

   ┌──────────────────────────┐
   │  status: "completed"     │  ◄── subscription.completed
   └──────────────────────────┘

subscription.completed delivery

subscription.completed is eventually consistent:
  1. When a Zest admin marks the underlying Bid Completed, the system attempts to emit subscription.completed synchronously.
  2. If that synchronous emit was skipped (admin path bypassed the helper, queue back-pressure, transient error), a reconciler actor runs on a 60-second cadence and emits the webhook for any partner-sourced Bid whose partner_webhook_subscription_completed_fired_at flag is still unset.
Practical implications for partners:
  • You will receive exactly one subscription.completed per subscription, even if the synchronous path drops it.
  • The webhook may arrive up to ~60 seconds after the admin action, not instantly.
  • Idempotency is enforced via the flag — the reconciler never double-fires.

Strict order: forms before fundings

POST .../fundings returns 409 conflict when called before POST .../forms succeeds. This enforces compliance: Zest must hold the signed subscription contract before any wire-transfer money is associated with the subscription. If you orchestrate uploads in parallel, gate the funding upload on a successful signed-form response (or on the corresponding webhook).

Per-row payload

Each row in the bulk request carries:
FieldRequiredNotes
partnerSubscriptionIdoptionalEchoed in the response and on every downstream webhook for correlation.
zestPersonIdrequiredReturned from POST /v1/investors.
lumpSumrequired{ "currency": "USD", "value": "50000.00" }. Currency is ISO 4217.
shareClassSlugrequiredSlug of the share class on the SPV.
The lumpSum.value is a decimal string — never a float — to avoid binary floating-point drift on monetary amounts.

Idempotency

Idempotency-Key is optional on POST /v1/spvs/{slug}/subscriptions. Replays under the same key + same body return the cached response (24h TTL). See Idempotency.

Upload constraints

Both forms and fundings endpoints accept multipart uploads with:
  • Max size: 10 MB.
  • Allowed content types: application/pdf, image/jpeg, image/png, image/webp.
Oversize → 413 payload_too_large. Wrong content-type → 415 unsupported_media_type.