Building on the API
A canonical end-to-end incorporation flow against the v1 API, walked through for Cayman Foundations, BVI Limited Companies, and Delaware LLCs
This cookbook walks through the full lifecycle of incorporating a real entity over the v1 API: from product discovery through to handing the customer back a paid, submitted-for-review company they own in their own EntityEngine account.
Three recipes are shown below — Cayman Foundation, BVI Limited Company, and Delaware LLC. The 12-step lifecycle is the same for all three. What differs is the CSP form schema returned by /catalog — which roles you must register, which CSP fields you must collect, which documents per role, and which conditional sections apply. The schema is the contract, not this cookbook. Always read it for the product you're building against.
Prerequisites
- An EntityEngine API key with scopes
entities:read,entities:write,parties:write,documents:write,invoices:write,collaborators:write. - The product opted into API availability by your EntityEngine admin.
- An https URL you can use as
success_url/cancel_urlfor hosted Stripe / Radom checkout. - A webhook endpoint subscribed to
payment.received,entity.status_changed,entity.feedback_created,invitation.accepted.
All requests below assume base URL https://app.entityengine.io/api/v1 and an Authorization: Bearer sk_live_... header.
The API flow
All API key integrations follow the same sequence. The entity stays in draft throughout — fully editable — until you explicitly submit it. Billing and submission are separate steps you control.
# 1. Create the entity (no invoice yet, fully editable).
curl -X POST https://app.entityengine.io/api/v1/entities \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"jurisdiction_product_id": "aecf91da-50bf-4d7e-9de8-76b28b0a0ccc",
"name": "Acme Foundation",
"client_reference": "bank-customer-9001"
}'
# data.id → ent_...
# data.status → "draft"
# 2. Configure across as many calls as you need.
# POST /entities/<ent_id>/parties
# POST /entities/<ent_id>/csp-data
# POST /entities/<ent_id>/documents
# POST /entities/<ent_id>/addons
# 3. (Optional) confirm all requirements are met.
curl https://app.entityengine.io/api/v1/entities/<ent_id>/requirements \
-H "Authorization: Bearer sk_live_..."
# 4. Issue the invoice + create a hosted Stripe checkout session in one call.
curl -X POST https://app.entityengine.io/api/v1/entities/<ent_id>/invoice \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"payment_method": "card",
"success_url": "https://bank.example.com/checkout/done",
"cancel_url": "https://bank.example.com/checkout/cancelled",
"customer_email": "founder@example.com"
}'
# data.invoice.id → inv_...
# data.checkout_session.checkout_url → forward to customer
# entity status stays "draft"
# 5. Wait for payment.received webhook, then submit.
curl -X POST https://app.entityengine.io/api/v1/entities/<ent_id>/submit \
-H "Authorization: Bearer sk_live_..."
# data.status → "submitted_for_review"
# Listen for entity.status_changed to track progress.Schema-driven flow — read this first
The /catalog response carries a schema object per product. It is the single source of truth for what the entity needs. Treat it as data your client interprets at runtime — do not hardcode field names or role lists.
Sections and scopes
| scope | What it means | Where to send the answers |
|---|---|---|
| entity | Entity-level fields (name, business activities, source of funds, regulatory questions). | POST /entities/:id/csp-data |
| share_structure | Share class definition (BVI Limited only — DE LLC and Cayman Foundation do not have this). | POST /entities/:id/share-classes |
| role:director, role:shareholder, role:member, role:supervisor, role:manager, role:ubo | A natural-person OR corporate-body OR foundation in that role. Section declares minimum, allows_corporate, allows_foundation, plus per-type field lists. | POST /entities/:id/parties with party_type: "person" | "corporate" | "foundation" |
Field metadata you must respect
canonical_field(e.g.entity.name,person.first_name) — cross-product field that maps to a typed column on EE's side. Pass it as the typed field on the relevant POST.csp_field_key(e.g.cayman.gnb_category,bvi.records_custodian_email) — jurisdiction-prefixed CSP-specific field. Send via the CSP data endpoint with the schema id.depends_on: { field, value }— show / require this field (or whole section) only when a condition on another field is met. Supports simple equality oroperator: "includes_any"for multi-value matching. Cascades: e.g. Caymanfoundation.governance_structurehides the Members section when set tosupervisor_onlyAND requires ≥1 supervisor when set tosupervisor_onlyormembers_and_supervisor.provided_by_agent: true— EE auto-populates this field from the CSP's registered office. Show the value fromschema.agent_addressas confirmation; do not ask the user for it.conditional_min: { field, value | value_in, minimum }— overrides the section's baseminimumwhen the named field equals the given value (or any of the values invalue_in).field_type— text, textarea, select, boolean, date, number, currency_amount, address, phone, nationality (array of ISO codes), share_class_ref. Render UI accordingly.documents— per role section, lists the document slots required. Each entry has aname(the slot label) andrequiredbool. Upload using the slot name asdocument_key.
The right pattern: render the schema dynamically, validate against required + depends_on + conditional_min, then POST. The recipes below show the canonical sequence per product.
The 12 canonical steps
- Discover available products via the catalog and read the
schema. - Create the entity (with any initial addons if known).
- Issue the invoice via
POST /entities/:id/invoiceand forward the hosted payment URL to the customer. - Invite the customer as a collaborator on their new entity.
- Define share classes (BVI only) — skip for Cayman Foundation and Delaware LLC.
- Register every role declared in the schema (directors, members, shareholders, supervisors, UBOs, etc).
- Submit the entity-scope CSP fields (and any role-scope CSP fields per party).
- Upload documents for every required slot in
schema.documentsand per role section. - Poll
GET /entities/:id/requirementsuntil it returnscomplete: true. - Wait for the
payment.receivedwebhook. - Submit the entity for review with
POST /entities/:id/submit. - Listen for
entity.status_changedwithnew_status: "incorporated"and surface registry documents to the customer.
Recipe 1 — Cayman Foundation
Roles required by the schema
director— minimum 1.member— minimum 0; section is hidden unlessfoundation.governance_structureismembers_onlyormembers_and_supervisor.supervisor— minimum 0; required (≥1) whenfoundation.governance_structureissupervisor_onlyormembers_and_supervisor.ubo— minimum 0; populate per BO register obligations.
Most demos use supervisor_only, which removes the Members section entirely and requires a supervisor.
curl https://app.entityengine.io/api/v1/catalog \
-H "Authorization: Bearer sk_live_..."
# Match jurisdiction.slug === "caymans" AND product.slug === "caymans-foundation-company".
# Capture:
# item.jurisdiction_product_id → for POST /entities
# item.schema.id → for POST /csp-data
# item.schema.sections → drives the chat / form
# item.schema.agent_address → registered office (auto-populated)curl -X POST https://app.entityengine.io/api/v1/entities \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"jurisdiction_product_id": "aecf91da-50bf-4d7e-9de8-76b28b0a0ccc",
"name": "Acme Foundation",
"addon_ids": [],
"client_reference": "bank-customer-9001"
}'
# data.id → ent_...
# data.status → "draft" (no invoice yet, fully editable)# Step 3 — Issue the incorporation invoice and open a hosted Stripe checkout session.
curl -X POST https://app.entityengine.io/api/v1/entities/<ent_id>/invoice \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"payment_method": "card",
"success_url": "https://chatbot.example.com/thanks?session={CHECKOUT_SESSION_ID}",
"cancel_url": "https://chatbot.example.com/cancelled",
"customer_email": "founder@example.com"
}'
# data.invoice.id → inv_...
# data.checkout_session.checkout_url → forward to customer
# Step 4 — Invite the customer so they can claim the entity in their own dashboard.
curl -X POST https://app.entityengine.io/api/v1/entities/<ent_id>/invitations \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{ "email": "founder@example.com", "role": "collaborator" }'# Director — natural person.
curl -X POST https://app.entityengine.io/api/v1/entities/<ent_id>/parties \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"party_type": "person",
"first_name": "Alice",
"last_name": "Director",
"email": "alice@example.com",
"phone": "+447700900123",
"date_of_birth": "1985-04-12",
"place_of_birth": "London, UK",
"nationalities": ["GB"],
"address": {
"street1": "10 Downing Street",
"city": "London",
"country": "GB",
"postcode": "SW1A 2AA"
},
"roles": ["director"]
}'
# If foundation.governance_structure is "supervisor_only" or "members_and_supervisor",
# register at least one supervisor (same payload, roles: ["supervisor"]).
# A director or supervisor can be a corporate body or foundation instead of a
# person — use POST /entities/<ent_id>/parties with party_type: "corporate" or "foundation".# Use the schema.id from the catalog and post the answers as field_key/value pairs.
# Field keys are the csp_field_key (or canonical_field) values from schema.sections[].fields[].
curl -X POST https://app.entityengine.io/api/v1/entities/<ent_id>/csp-data \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"csp_form_schema_id": "e92ace31-51a6-4587-bf65-49d4ec5a7362",
"fields": [
{ "field_key": "cayman.alternate_name", "value": "Acme Stewardship Foundation" },
{ "field_key": "foundation.purpose", "value": "Stewardship of an open-source treasury." },
{ "field_key": "foundation.governance_structure", "value": "supervisor_only" },
{ "field_key": "cayman.tax_exemption_certificate", "value": false },
{ "field_key": "cayman.corporate_seal", "value": false },
{ "field_key": "cayman.share_certificates", "value": false },
{ "field_key": "cayman.cima_licensing", "value": false },
{ "field_key": "cayman.gnb_category", "value": "Holding Company (securities)" },
{ "field_key": "cayman.business_activities", "value": "Holds treasury assets for the Foo DAO." },
{ "field_key": "cayman.business_locations", "value": "Cayman Islands, United Kingdom" },
{ "field_key": "cayman.multiple_structure_layers", "value": false },
{ "field_key": "cayman.source_of_funds", "value": "Initial protocol grant." },
{ "field_key": "cayman.source_of_wealth", "value": "Donations/Inheritance" },
{ "field_key": "cayman.is_domestic_company", "value": false },
{ "field_key": "cayman.is_investment_fund", "value": false },
{ "field_key": "cayman.is_tax_resident_outside", "value": false },
{ "field_key": "cayman.bo_out_of_scope", "value": false },
{ "field_key": "cayman.carries_relevant_activities", "value": false }
]
}'# Each role section in the schema has a documents.{person|entity|foundation} list.
# For a director who is a person, the schema requires:
# - "Certified passport copy"
# - "Proof of address"
#
# Upload each one against that party using the slot name as document_key.
curl -X POST https://app.entityengine.io/api/v1/entities/<ent_id>/documents \
-H "Authorization: Bearer sk_live_..." \
-F "scope=party" \
-F "party_id=<alice_party_id>" \
-F "document_key=Certified passport copy" \
-F "file=@/local/alice-passport.pdf"
# Entity-scope documents (e.g. addon-related) use scope=entity and no party_id.# Step 9 — Poll until requirements are clear.
curl https://app.entityengine.io/api/v1/entities/<ent_id>/requirements \
-H "Authorization: Bearer sk_live_..."
# Step 10 — Wait for the payment.received webhook for the entity's invoice.
# Step 11 — Submit for CSP review. Returns 4xx with structured reasons if anything is off.
curl -X POST https://app.entityengine.io/api/v1/entities/<ent_id>/submit \
-H "Authorization: Bearer sk_live_..."
# Step 12 — Listen for entity.status_changed with new_status: "incorporated";
# then GET /entities/<ent_id>/documents to surface the registry pack.Recipe 2 — BVI Limited Company
Roles required by the schema
director— minimum 1. May be a person or a corporate body.shareholder— minimum 1. Carries relationship fields (share_class+shares_held).ubo— minimum 1.
Plus a share_structure section — you must create at least one share class before issuing shareholdings.
curl -X POST https://app.entityengine.io/api/v1/entities \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"jurisdiction_product_id": "be9a0966-794a-410f-95c4-a244d298b221",
"name": "Acme Holdings",
"client_reference": "bank-customer-9001"
}'
# Then at step 3, issue the invoice with Radom (crypto) checkout:
# POST /entities/<ent_id>/invoice with payment_method: "crypto"curl -X POST https://app.entityengine.io/api/v1/entities/<ent_id>/share-classes \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"name": "Ordinary",
"currency": "USD",
"par_value": 1,
"voting_rights": true,
"dividend_rights": true,
"authorized_shares": 50000
}'
# Capture share_class_id for the shareholding step below.# Director.
curl -X POST https://app.entityengine.io/api/v1/entities/<ent_id>/parties \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"party_type": "person",
"first_name": "Alice",
"last_name": "Director",
"email": "alice@example.com",
"phone": "+447700900123",
"date_of_birth": "1985-04-12",
"place_of_birth": "London, UK",
"nationalities": ["GB"],
"address": {
"street1": "10 Downing Street",
"city": "London",
"country": "GB",
"postcode": "SW1A 2AA"
},
"roles": ["director","shareholder","ubo"]
}'
# Issue 100 shares to Alice (use the share_class_id captured above).
curl -X POST https://app.entityengine.io/api/v1/entities/<ent_id>/shareholdings \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"party_id": "<alice_party_id>",
"share_class_id": "<share_class_id>",
"shares_held": 100
}'curl -X POST https://app.entityengine.io/api/v1/entities/<ent_id>/csp-data \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"csp_form_schema_id": "71624bc1-7e42-4c85-907b-96cecb315cc0",
"fields": [
{ "field_key": "bvi.purpose_category", "value": "Holding of Assets" },
{ "field_key": "bvi.purpose_details", "value": "Holds intellectual property used by Acme group entities." },
{ "field_key": "bvi.source_of_funds", "value": "Founder capital." },
{ "field_key": "bvi.estimated_annual_turnover", "value": 250000 },
{ "field_key": "bvi.company_maintain_bank_accounts", "value": true },
{ "field_key": "bvi.bank_account_countries", "value": ["GB","SG"] },
{ "field_key": "bvi.records_custodian_boolean", "value": true }
]
}'
# UBO-scope CSP fields (occupation, source_of_funds, etc) are submitted per
# party with POST /entities/<ent_id>/party-csp-data.Recipe 3 — Delaware LLC
Roles required by the schema
member— minimum 1.manager— minimum 1, but only whendelaware.management_structure = "Manager-Managed". Skip entirely for member-managed LLCs.
No share classes, no shareholders, no UBO section. Member onboarding is intentionally minimal — Delaware LLCs only require first name + last name per member at formation.
curl -X POST https://app.entityengine.io/api/v1/entities \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"jurisdiction_product_id": "e4e98e70-3ab4-4aaf-af48-ae6138d8c8b8",
"name": "Acme Labs",
"client_reference": "bank-customer-9001"
}'
# Then at step 3, issue the invoice with Stripe checkout:
# POST /entities/<ent_id>/invoice with payment_method: "card"# Member — only first_name + last_name are mandatory per the schema.
curl -X POST https://app.entityengine.io/api/v1/entities/<ent_id>/parties \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"party_type": "person",
"first_name": "Alice",
"last_name": "Member",
"roles": ["member"]
}'
# Skip the next call if delaware.management_structure === "Member-Managed".
curl -X POST https://app.entityengine.io/api/v1/entities/<ent_id>/parties \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"party_type": "person",
"first_name": "Bob",
"last_name": "Manager",
"roles": ["manager"]
}'curl -X POST https://app.entityengine.io/api/v1/entities/<ent_id>/csp-data \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"csp_form_schema_id": "24646997-2ecd-44a6-99ba-7ffd66acddc2",
"fields": [
{ "field_key": "entity.business_activity", "value": "Software product development." },
{ "field_key": "delaware.management_structure", "value": "Member-Managed" }
]
}'Delaware LLC requires no per-person KYC documents at formation. Steps 8–12 are the same as the Cayman recipe — poll requirements, wait for payment.received, submit, listen for entity.status_changed with new_status: "incorporated".
Post-incorporation — selling more addons
Once entity.status_changed fires with new_status: "incorporated", additional addons follow a different billing path automatically. Same endpoint, the response tells you what happened.
curl -X POST https://app.entityengine.io/api/v1/entities/<ent_id>/addons \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"addon_slug": "nominee-director",
"payment_provider": "stripe",
"success_url": "https://chatbot.example.com/thanks",
"cancel_url": "https://chatbot.example.com/cancelled",
"customer_email": "founder@example.com"
}'
# Response carries invoice.action:
# "appended" → bolted onto the (still unpaid) setup invoice; old session was regenerated.
# "created" → fresh api_topup invoice raised; new checkout session URL returned.
# If GET /entities/<ent_id>/addons shows requires_survey: true for that addon,
# call /addons/<slug>/requirements then /submit before the addon goes active.Recommended webhook subscriptions
| Event | Why subscribe |
|---|---|
| payment.session_created | Refresh the checkout link in your UI when it changes. |
| payment.received | Unblock the “ready to submit” step in your UX. |
| payment.session_expired | Hosted checkout session expired before payment — regenerate via POST /invoices/{id}/checkout-session and re-prompt. |
| invitation.accepted | Show the customer they now own the entity. |
| addon.invoiced | Track post-incorporation top-ups. |
| entity.status_changed | Fired on every status transition. Check new_status: submitted_for_review to mark as handed off, returned_for_changes to prompt for corrections, incorporated to surface registry docs. |
Sandbox vs live
Sandbox API keys do not have a payment gateway attached. To test the lifecycle without real charges, omit payment_provider (the invoice will fall back to bank-transfer mode) and simulate payment with POST /sandbox/invoices/{invoiceId}/pay. Once you're confident, swap to a live key and add the payment_provider field to your entity-creation call.