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

# Sell a new contract

> The full new-business journey, from product configuration to an in-force contract

New business is the journey from "a prospect wants cover" to "a contract is in force". It runs through
five statuses; most transitions are a call you make, and the last one is not:

`POLICY_CREATED` → the contract exists, unpriced. `POLICY_QUOTED` → the risk has been priced.
`POLICY_CONFIRMED` → the terms are locked and the paperwork can be produced. `POLICY_SIGNED` → the
policyholder has committed. `POLICY_STARTED` → cover has begun, which on some products waits for the first
premium as well as the start date.

Statuses carry their entity prefix in every response, and assets and customers use `ASSET_*` and
`CUSTOMER_*`.

This page walks the whole journey. It assumes you have credentials and can call the API — see
[Authentication](/api-reference/authentication) — and that you have read
[Read the product configuration](/get-started/product-configuration), because which fields you must
send at each step comes from the product, not from this page.

## The journey at a glance

One example run. The product's own configuration decides parts of it — whether a payment method is needed
before signature, which fields are required at which step, whether cover starts on signature — so read
yours rather than assuming this shape.

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant You as Your system
    participant K as Korint API
    participant S as Stripe
    participant Y as Signature provider

    You->>K: GET /config/{productId}
    K-->>You: field lists, payment and signature rules

    You->>K: POST /customers?productId=...
    K-->>You: customerId + initializedWith { policy, branch }

    Note over You,K: every write below carries ?branchId=...

    You->>K: PATCH /customers/{customerId}
    Note over K: company fields enriched from the registry
    You->>K: POST /assets  (policyId, startedAt, customFields)
    You->>K: PATCH /policies/{policyId}  (invoicingConfig from billing.defaultConfig)

    opt Preview a price without committing
        You->>K: POST /policies/{policyId}/calculate-quote
        K-->>You: premium breakdown, nothing persisted
    end

    You->>K: POST /policies/{policyId}/quote
    K-->>You: quote persisted on the policy

    You->>K: POST /policies/{policyId}/confirm?branchId=...
    Note over K: every CONFIRMATION field must be present

    opt Direct debit products
        You->>K: GET /customers/{customerId}/payment-methods/secret
        K-->>You: clientSecret
        You->>S: confirm the setup with Stripe.js
        You->>K: POST /customers/{id}/payment-methods/{pmId}/signature
    end

    You->>K: POST /policies/{policyId}/signature
    K->>Y: signature request created
    K-->>You: signatures[] with signer links

    loop Until signed
        You->>K: GET /signature/by-external-id/{policyId}
    end

    Note over K: activation follows the start date and,<br/>when required, the first payment
    You->>K: GET /policies/{policyId}
    K-->>You: status
```

## Step by step

<Steps>
  <Step title="Read the product configuration">
    ```bash theme={null}
    curl 'https://api.sandbox.korint.io/config/<productId>' \
      --header 'tenant: <tenant>'
    ```

    This call is public — no token, just the `tenant` header. Every step after it needs one.

    Keep the response. Its six field lists tell you what to send in the next four steps, and its
    `payment`, `billing` and `signature` sections tell you whether this product needs a payment method
    before signature and how signing is delivered.

    If you are building a form on top of this, read
    [Should your form be config-driven?](/get-started/product-configuration#should-your-form-be-config-driven)
    first — the short version is that the config drives which fields you ask for and how they behave, while
    labels, ordering and grouping are yours to write, because the config holds none of them.
  </Step>

  <Step title="Initialize a customer, and with it a policy">
    One call creates the policyholder, the policy, and the branch you will work on. The body is optional —
    send `{}` for a direct sale, or name the distributing firm with `brokerageFirmId`:

    ```bash theme={null}
    curl -X POST 'https://api.sandbox.korint.io/customers?productId=<productId>' \
      --header 'Authorization: Bearer <access_token>' \
      --header 'tenant: <tenant>' \
      --header 'Content-Type: application/json' \
      --data '{ "brokerageFirmId": "<brokerageFirmId>" }'
    ```

    ```json Response theme={null}
    {
      "type": "CUSTOMER",
      "id": "<customerId>",
      "initializedWith": { "policy": "<policyId>", "branch": "<branchId>" }
    }
    ```

    <Warning>
      Keep `initializedWith.branch`. Confirmation requires a `branchId` query parameter, and a new-business
      policy carries more than one branch. If you lose it, list the open branches with
      `GET /policies/{policyId}/branches`.
    </Warning>

    A branch is a working copy of the contract. New business happens on its own branch; mid-term
    adjustments and renewals each get theirs. Reads and writes are branch-scoped, which is why querying
    a policy without the right `branchId` can look as though your data never landed.
  </Step>

  <Step title="Fill in the customer">
    Custom fields go in `customFields`; the customer's aggregate fields (`name`, `role`, `siret`) are
    top-level:

    ```bash theme={null}
    curl -X PATCH 'https://api.sandbox.korint.io/customers/<customerId>?branchId=<branchId>' \
      --header 'Authorization: Bearer <access_token>' \
      --header 'tenant: <tenant>' \
      --header 'Content-Type: application/json' \
      --data '{
        "customFields": [
          { "key": "contactFirstName", "value": "Camille" },
          { "key": "contactLastName", "value": "Durand" },
          { "key": "contactEmail", "value": "camille.durand@example.com" },
          { "key": "contactBirthDate", "value": "1985-04-12" }
        ]
      }'
    ```

    <Warning>
      `branchId` is marked optional in the reference, but omit it here and any field whose `requiresOnChange`
      is `QUOTE` — which on most products is every field you need — is refused:

      ```
      409 FIELDS_DO_NOT_MEET_REQUIREMENTS
        contactFirstName: "cannot be modified with this context
                           (not configured as requiring NOTHING on change)"
      ```

      The error names the fields, not the missing parameter, which sends you looking in the wrong place. Pass
      the `branchId` from step 2 on every write.
    </Warning>

    Field keys are per-product — read them from the configuration rather than copying these. Note the birth
    date: a `DATE` field wants `1985-04-12`, and rejects a full ISO datetime.

    <Note>
      For a company product, expect the company block you read back to differ from what you sent. Supplying
      the SIRET triggers a registry lookup that overwrites company name, address, postcode, city, activity
      code and legal statuses, and nulls legal statuses the registry does not return. This is intended
      behaviour, explained in
      [Fields the platform owns](/get-started/product-configuration#fields-the-platform-owns).
    </Note>
  </Step>

  <Step title="Create the insured items">
    `startedAt` is an aggregate field on most products — a top-level property, not a custom field:

    ```bash theme={null}
    curl -X POST 'https://api.sandbox.korint.io/assets?branchId=<branchId>' \
      --header 'Authorization: Bearer <access_token>' \
      --header 'tenant: <tenant>' \
      --header 'Content-Type: application/json' \
      --data '{
        "policyId": "<policyId>",
        "startedAt": "2026-08-31T22:00:00.000Z",
        "customFields": [
          { "key": "postcode", "value": "75011" },
          { "key": "surfaceArea", "value": 120 }
        ]
      }'
    ```

    <Warning>
      That `startedAt` is **Paris-local midnight**, not UTC midnight — `22:00:00Z` the previous day in summer,
      `23:00:00Z` in winter. UTC midnight silently buys 364.92 days of cover for a full year's premium, with no
      error anywhere. Why, and how to convert:
      [Dates and times](/get-started/product-configuration#dates-and-times).
    </Warning>

    Check the product's `assetAggregateFields` rather than assuming: if `startedAt` is declared there with
    `requiredFor: QUOTE`, quoting fails without it. Use `POST /assets/batch/create` when a contract covers
    many items.
  </Step>

  <Step title="Set the contract-level terms">
    The policy's aggregate fields — how often the customer is invoiced, and who pays — are top-level
    properties of `PATCH /policies/{policyId}`. Two different kinds of data live there, and they are not
    collected at the same moment:

    * **`invoicingConfig` is a term you set.** It is required before pricing, so it belongs in a call your
      code makes as it prepares the quote.
    * **Policy custom fields are usually answers a person gives.** They can be sent any time before the
      action their `requiredFor` names — often confirmation, not quote.

    <Warning>
      This example sends both in one call because a script can. If you are building an interface, do **not**
      copy that shape into the step that opens your pricing screen: the custom fields would be sent before the
      customer has typed anything, and never sent again. On a product where no policy field is priced or
      printed, nothing fails — the answers are simply lost.
    </Warning>

    Take `invoicingConfig` straight from the product's `billing.defaultConfig` — that is the shape it
    expects, and there is nothing for you to invent:

    ```bash theme={null}
    curl -X PATCH 'https://api.sandbox.korint.io/policies/<policyId>?branchId=<branchId>' \
      --header 'Authorization: Bearer <access_token>' \
      --header 'tenant: <tenant>' \
      --header 'Content-Type: application/json' \
      --data '{
        "invoicingConfig": {
          "frequency": "MONTHLY",
          "timing": "IN_ADVANCE",
          "firstPayment": "EARLY_PAYMENT_AT_SIGNATURE",
          "earlyPayment": { "unit": "MONTH", "amount": 2 }
        },
        "customFields": [
          { "key": "contractAssistanceOption", "value": false }
        ]
      }'
    ```

    <Warning>
      Send every aggregate field the product declares, at the step its `requiredFor` demands. On the demo
      products that means `invoicingConfig`, required at **QUOTE** — so this call comes before pricing, not
      after it.

      Some products additionally declare `billingAssignment` (`{ configId, defaultPayerId }`), required at
      confirmation; none of the demo products do. Read `policyAggregateFields` instead of assuming either
      way, because the error names the field rather than the step it was needed for.
    </Warning>
  </Step>

  <Step title="Price the risk">
    Two endpoints price a contract, and the difference matters:

    `POST /policies/{policyId}/calculate-quote` computes a premium and **persists nothing**. Use it to
    show a price, to compare options, or to try overrides — it accepts `customFieldsToOverride`,
    `brokerageFeesOverride`, `periodDurationOverride` and `assetIds` for exactly that. It also tolerates
    assets without a `startedAt`.

    `POST /policies/{policyId}/quote` computes the premium and **writes it to the policy**. This is the
    call that moves the contract forward; confirmation works from the quote it stored.

    Both are covered in detail in [Quotes](/concepts/quotes#pricing-through-the-api).

    `options` is an array with **one entry per asset**, each naming the coverage the customer chose. The
    permitted values come from the config's `quote` object — `quote.availableTiers`,
    `quote.availableExcesses` and `quote.availablePerils`. Not `publicQuoteConfig`, which is a smaller
    summary carrying only `productId`, `defaultTier` and `tiers`:

    ```bash theme={null}
    curl -X POST 'https://api.sandbox.korint.io/policies/<policyId>/quote?branchId=<branchId>' \
      --header 'Authorization: Bearer <access_token>' \
      --header 'tenant: <tenant>' \
      --header 'Content-Type: application/json' \
      --data '{
        "options": [
          { "assetId": "<assetId>", "tier": "STANDARD", "excess": "DEFAULT" }
        ]
      }'
    ```

    Add `perils` to an option to sell optional cover, where the product offers any. Check
    `quote.availablePerils` and look at each peril's `options` array first: a peril with a single permitted
    option is not a choice, and sending another value is rejected with
    `INVALID_QUOTE_OPTIONS_INVALID_PERIL_OPTION`.

    <Note>
      `POST /policies/{policyId}/quote` returns an empty body. It records the quote rather than handing it
      back, so read the result afterwards — or use `calculate-quote`, which does return the full breakdown.

      Read it from the right place: the premium lands on the policy, but **the chosen tier is on the asset**
      (`GET /assets/{assetId}` → `quote.options.tier`), because cover is chosen per insured item. See
      [Read a contract back](/get-started/reading-a-contract).
    </Note>

    Pricing can fail on underwriting grounds rather than on your payload — an ineligible risk, a value
    outside the carrier's appetite. Those failures are recorded against the policy, so a rejected quote
    is an outcome to handle, not necessarily a bug in your request.
  </Step>

  <Step title="Confirm the contract">
    Confirmation locks the terms:

    ```bash theme={null}
    curl -X POST 'https://api.sandbox.korint.io/policies/<policyId>/confirm?branchId=<branchId>' \
      --header 'Authorization: Bearer <access_token>' \
      --header 'tenant: <tenant>'
    ```

    Every field whose `requiredFor` is `CREATION`, `QUOTE` or `CONFIRMATION` must be present, across the
    customer, the policy and every asset. This is where an integration that treated aggregate fields as
    custom fields — or never sent them at all — discovers the gap.
  </Step>

  <Step title="Collect a payment method, if the product needs one first">
    Whether payment comes before signature is a product decision, and it is in the configuration. See
    [Payments](/get-started/payments) for the mechanics, the Stripe hand-off, and the mandate.
  </Step>

  <Step title="Send it for signature">
    ```bash theme={null}
    curl -X POST 'https://api.sandbox.korint.io/policies/<policyId>/signature?branchId=<branchId>' \
      --header 'Authorization: Bearer <access_token>' \
      --header 'tenant: <tenant>' \
      --header 'Content-Type: application/json' \
      --data '{ "deliveryMode": "EMAIL" }'
    ```

    <Warning>
      Pass `branchId` here too, even though the reference marks it optional. The documents offered for signature
      are the ones on the branch you name, so omitting it looks for the paperwork on the base contract and fails
      with `425 NO_UNIQUE_DOCUMENT_TYPE_READY_TO_BE_SIGNED` and `documentNumber: 0` — while
      `GET /policies/{policyId}/documents?branchId=<branchId>` plainly lists the document.
    </Warning>

    The response lists the signature requests and their signers. Signing happens outside your system —
    the policyholder receives it by email, or you place them in front of the signer link. Either way,
    completion is something you observe rather than receive: see
    [Asynchronous operations](/get-started/asynchronous-operations).
  </Step>

  <Step title="Watch it come into force">
    Activation is not a call you normally make. A signed contract comes into force when its start date
    arrives and, on products that require it, when the first premium has been collected. Poll
    `GET /policies/{policyId}` and read the status.

    A contract that is signed but not yet in force is the normal state for a future-dated start or an
    uncollected first premium — not a failure. There is nothing to call: wait for the start date, or
    collect the premium.

    <Warning>
      `POST /policies/{policyId}/activate` is not the escape hatch its name suggests. It only brings a
      **suspended** contract back into force, and rejects anything else with
      `409 CONFLICTING_POLICY_STATUS`. On a signed-but-not-yet-in-force contract — the case above — it
      always fails. Nothing in the new-business journey calls it.
    </Warning>
  </Step>
</Steps>

## Where integrations get stuck

* **Fields rejected as "cannot be modified with this context".** You omitted `branchId` on a write.
* **A required field you believe you sent.** Check which of the six configuration lists it came from.
  Aggregate fields are top-level properties; custom fields go in `customFields`.
* **Quoting refuses fields you treated as read-only.** An `integrationKey` does not mean the platform
  fills the field; only the registry family is filled for you. See
  [Fields mapped onto an integration](/get-started/product-configuration#fields-mapped-onto-an-integration-integrationkey).
* **`PAYMENT_CUSTOMER_NOT_FOUND` when asking for a setup secret.** The payment customer is created when
  the policy is quoted. Quote first.
* **The contract is two hours short.** `startedAt` needs to be Paris-local midnight.
* **Confirmation rejected on a field the customer never sees.** `invoicingConfig` and
  `billingAssignment` are contract terms you set, not customer input.
* **A value you sent came back different.** A field with an `integrationKey` is owned by its
  integration; company identity comes from the registry.
* **Nothing appears to have been saved.** You are probably reading a different branch than you wrote.
* **The contract is signed but not in force.** Check the start date, then the first premium.
