Skip to main content

Creating and listing pre-registrations

Use createPreRegistration to prepare an Altersvorsorgedepot Plus pre-registration for a German resident directly from your platform. It applies the same validation, broker attribution and email rules as Partner Life. Submit the complete answers after the broker's conversation with the customer; keep intermediate states in your own system.

This step creates no insurance-system offer or app account. The result depends on the customer's relationship to the broker:

CustomerInput choiceResult
Existing customer with an app accountexistingCustomerCreatedPreRegistration: answers are saved on that account, then the API tries to send the usual preparation email.
Existing customer without an app accountexistingCustomerPreRegistrationInvitationSent: answers wait behind an invitation addressed to the email on record.
New customernewCustomerPreRegistrationInvitationSent: answers wait behind an invitation addressed to the supplied email.

CreatedPreRegistration confirms that the record was saved. Preparation email sending is best-effort: a missing recipient or a mail-provider failure can leave the saved record without an email. The customer can still find it in the app. Neither success result confirms mailbox delivery; do not resubmit the mutation to retry an email.

For an invitation, the pre-registration is created in the account only after the recipient signs in or registers, verifies their email and confirms their identity. Existing accounts are preserved. An existing pre-registration is never overwritten.

AI integration prompt​

Copy this prompt into your AI coding assistant to implement the existing-customer and new-customer paths. It includes the request, schema types, access rules, email behavior and retry handling from this guide.

Create an AVD Plus pre-registration
Preview prompt
Build an integration for creating Altersvorsorgedepot Plus pre-registrations for German residents through the Partner Platform GraphQL API. Support both existing customers and new customers.

## Authentication
- Endpoint: https://api.stable.life.li/graphql/partner-platform (test) or https://api.life.li/graphql/partner-platform (production)
- Set header: Authorization: Bearer YOUR-API-KEY
- Set header: x-partner-id: YOUR-PARTNER-ID
- All requests are POST with Content-Type: application/json
- The body is: { "query": "...", "variables": { ... } }

## Implementation requirements
- Use the partner's existing API credential with the partner-platform route. Keep credentials on the server and out of source control, logs and browser code.
- Use the guide below as the flow contract. Map complete answers collected by the broker; ask for missing answers instead of inventing them. Collect the invitation declaration explicitly rather than automatically setting it to true.
- Support existing customers by ID or email and new customers by their supplied details. Persist the returned pre-registration or invitation ID and handle the two success outcomes separately.
- Select the acting broker with x-partner-id. Never widen the customer scope or supply broker IDs or app-account IDs in the mutation input. Handle out-of-book customers without trying another broker automatically.
- Retrieve eligible funds for the acting broker and Altersvorsorgedepot Plus. The example ISIN is a placeholder; replace it before submission. Use integer EUR cents and validate fund percentages.
- Handle both top-level GraphQL errors and every declared union outcome. Do not automatically retry this mutation after a timeout or uncertain response; it has no request idempotency key.
- Preserve existing accounts and pre-registrations. A saved pre-registration confirms the record only; preparation email sending is best-effort. An invitation success does not prove email delivery or disclose account status.
- Also explain the alternative: register a customer with createCustomer, keep its customerID, then select that customer in Partner Life to prepare the AVD flow there. Customer registration alone sends no AVD invitation and creates no pre-registration.
- Add integration tests for existing customers with and without an app account, new customers, customer access denial, validation, missing declaration, invitation limits and uncertain responses. Use synthetic customer data in the stable environment; keep personal data out of logs.

## Flow guide

# Creating and listing pre-registrations

Use `createPreRegistration` to prepare an Altersvorsorgedepot Plus pre-registration for a German resident directly from your platform. It applies the same validation, broker attribution and email rules as Partner Life. Submit the complete answers after the broker's conversation with the customer; keep intermediate states in your own system.

This step creates no insurance-system offer or app account. The result depends on the customer's relationship to the broker:

| Customer | Input choice | Result |
|----------|--------------|--------|
| Existing customer with an app account | `existingCustomer` | `CreatedPreRegistration`: answers are saved on that account, then the API tries to send the usual preparation email. |
| Existing customer without an app account | `existingCustomer` | `PreRegistrationInvitationSent`: answers wait behind an invitation addressed to the email on record. |
| New customer | `newCustomer` | `PreRegistrationInvitationSent`: answers wait behind an invitation addressed to the supplied email. |

`CreatedPreRegistration` confirms that the record was saved. Preparation email sending is best-effort: a missing recipient or a mail-provider failure can leave the saved record without an email. The customer can still find it in the app. Neither success result confirms mailbox delivery; do not resubmit the mutation to retry an email.

For an invitation, the pre-registration is created in the account only after the recipient signs in or registers, verifies their email and confirms their identity. Existing accounts are preserved. An existing pre-registration is never overwritten.

## AI integration prompt

Copy this prompt into your AI coding assistant to implement the existing-customer and new-customer paths. It includes the request, schema types, access rules, email behavior and retry handling from this guide.

## Access and broker attribution

Use the authentication described in [Platform GraphQL API](https://partner.life.li/integration-docs/platform-integrations/graphql-api). Every request needs your API key and an `x-partner-id` header containing the acting broker's Lifeware ID. Your key must be authorized to act for that broker.

Your integration needs `CREATE_OFFER_V2`. The responsible broker must have new-business access and the German market. A colleague acts under their parent broker: the colleague remains the author, while the responsible broker receives the attribution and uses their invitation allowance. The API resolves these relationships; neither a broker ID nor an app user ID belongs in the input.

Pensionfriend and Nettowelt already have the required API permission. Pensionfriend can act for its configured broker IDs; Nettowelt can also act for brokers in its configured broker trees. Both remain subject to the broker and customer access checks above.

The integration's access is checked against the current hierarchy. A colleague with an individual seat can select only customers visible to that seat; a back-office colleague can act for the broker's customer book.

## Complete request

Always request `__typename` and `ErrorInterface.message`, then handle the two success types separately.

```graphql
mutation CreatePreRegistration($input: CreatePreRegistrationInput!) {
  createPreRegistration(input: $input) {
    __typename
    ... on CreatedPreRegistration {
      preRegistrationId
    }
    ... on PreRegistrationInvitationSent {
      invitationId
    }
    ... on ErrorInterface {
      message
    }
    ... on PreRegistrationInputValidationError {
      issues {
        path
        code
        message
      }
    }
    ... on PreRegistrationInvitationRefusedError {
      reason
      retryAt
    }
  }
}
```

The variables below show a new customer with direct subsidy entitlement, no children and no existing pension contracts. Replace the placeholder ISIN with an eligible fund before sending the request. Altersvorsorgedepot Plus takes its funds from the Liechtenstein Life Invest range that `availableFunds(product: LIECHTENSTEIN_LIFE_INVEST)` returns, within the limits listed below the example.

```json
{
  "input": {
    "liechtensteinLifeAVDPlus": {
      "customer": {
        "newCustomer": {
          "firstName": "Alex",
          "lastName": "Example",
          "dateOfBirth": "1990-05-17",
          "gender": "DIVERSE",
          "email": "alex@example.com",
          "address": {
            "street": "Example Street",
            "houseNumber": "1",
            "zip": "10115",
            "city": "Berlin",
            "country": "DE"
          },
          "taxResidencyDE": true
        }
      },
      "contribution": {
        "monthlyContribution": 20000,
        "retirementAge": 67
      },
      "subsidyEntitlement": {
        "type": "DIRECT",
        "employmentType": "EMPLOYED",
        "children": []
      },
      "existingContracts": [],
      "productConfiguration": {
        "payoutForm": "LIFELONG",
        "lumpSumPercent": 0,
        "pensionGuaranteeYears": 0,
        "annualIncreasePercent": 0
      },
      "fundAllocations": [
        {
          "isin": "REPLACE_WITH_ELIGIBLE_FUND_ISIN",
          "percent": 100
        }
      ],
      "invitationDeclaration": {
        "confirmed": true,
        "locale": "EN",
        "mailLocale": "EN"
      }
    }
  }
}
```

`monthlyContribution` is in EUR cents: `20000` means EUR 200 per month. Partner Life's age-dependent contribution minimum and retirement-age rules also apply here. Fund ISINs must be distinct; percentages must be positive whole numbers totalling 100. Each fund must have a known risk class from 1 to 5. Funds under Swiss law (ISINs starting with `CH`) and Liechtenstein Life's internal strategies (IDs starting with `LLA`) are not admitted, although `availableFunds` lists them for Liechtenstein Life Invest. A refused fund is reported under the issue code `fundRiskAboveLimit`, whatever the reason. The placeholder above is not a usable fund selection.

`taxResidencyDE` must be explicitly `true`, and the residence country must be `DE`. For indirect entitlement, supply the spouse's required details and `notPermanentlySeparated: true`. Supply `children: []` and `existingContracts: []` when those lists are empty.

## Existing customers

Replace `customer.newCustomer` with `customer.existingCustomer`. Set `by` to either the customer ID returned by the `customer` query or their email, and put the complete person answers under `person`:

```json
{
  "existingCustomer": {
    "by": {
      "ID": "00000000-0000-4000-8000-000000000001"
    },
    "person": {
      "firstName": "Alex",
      "lastName": "Example",
      "dateOfBirth": "1990-05-17",
      "gender": "DIVERSE",
      "email": "alex@example.com",
      "address": {
        "street": "Example Street",
        "houseNumber": "1",
        "zip": "10115",
        "city": "Berlin",
        "country": "DE"
      },
      "taxResidencyDE": true
    }
  }
}
```

Use the real customer ID in place of the example UUID. To identify them by email, use `"by": { "email": "alex@example.com" }` instead. Supply exactly one identifier and exactly one customer choice.

When several customers in scope share an email, the API returns an `ambiguousCustomer` validation issue. Select the person by ID. Existing customers must be natural persons; companies cannot hold a personal pension pre-registration.

The customer must belong to your broker subtree through a current policy, offer or eligible direct broker assignment from `createCustomer`. The API resolves account ownership again before writing. If they have no account, the invitation goes to their email **on record**, even if `person.email` contains a different answer. This operation does not update that stored email.

Selecting an existing customer outside the acting broker's book or colleague seat returns `CustomerNotFoundError`, whether identified by ID or email. The request creates no pre-registration or invitation and sends no email. Authorization to act for several brokers does not combine their customer books: each request uses the scope of its `x-partner-id`.

Use `existingCustomer` for a customer already in your book. A new-customer request naming an email found in your own customer search is refused with `RECIPIENT_IS_EXISTING_CUSTOMER`. An email outside your book receives no account-ownership disclosure.

## Register a customer, then continue in Partner Life

You can instead [register the customer using `createCustomer`](https://partner.life.li/integration-docs/platform-integrations/creating-offers#registering-a-customer) and keep its returned `customerID`. The customer is assigned to the broker selected by `x-partner-id` and can be selected in Partner Life's AVD customer search before an offer or contract exists, subject to the same current broker and colleague access rules.

The broker can then prepare the pre-registration in Partner Life, including the fund selection. Customer registration alone creates no AVD pre-registration, app account or offer and sends no AVD invitation; the pre-registration submission triggers the applicable preparation email or invitation.

## Invitations and deferred details

Include `invitationDeclaration` whenever the recipient needs an invitation. Set `confirmed: true` only when the broker collected these answers in conversation with the customer and the customer knows they will receive an invitation by email. `locale` records the declaration's language; `mailLocale` selects the invitation language and defaults to `DE`. Supported values are `DE`, `EN`, `FR` and `IT`. An existing customer with an app account can omit this declaration.

Invitations retain the submitted answers while the recipient confirms their identity. They expire after 14 days and share Partner Life's limit of 20 creations per responsible broker in a rolling 24-hour window. Partner and recipient eligibility restrictions continue to apply. Treat `PreRegistrationInvitationSent` as an accepted invitation: it does not confirm mailbox delivery or disclose the recipient's account status.

Tax and social-insurance identifiers and bank details can be deferred until signing; supplied identifiers must pass the applicable validation. Bank details grant no SEPA mandate. Final legal acknowledgments and transfer mandates are collected later from the customer, not accepted through this pre-registration API.

## Listing pre-registrations

Use the `preRegistrations` query to read back the Vormerkungen a Vermittler prepared in the broker's tree, whether in Partner Life or through `createPreRegistration`. Entries a colleague prepared before leaving stay listed. It lists these kinds of entry:

- `PreRegistration`: held in a customer's app account. The customer can see and change it in the app.
- `PreRegistrationInvitation`: an invitation waiting for the person to register and claim the answers.

As in Partner Life, pre-registrations a customer filled in themselves are listed too, complete or incomplete, without a copied broker link or its sharing consent. A pre-registration another tree's Vermittler prepared for a customer who is also attributed to your tree is listed, as Partner Life shows it to you too. Your integration needs the `PRE_REGISTRATION_READ` permission. The broker in `x-partner-id` needs the same broker access as for `createPreRegistration`. Pensionfriend has this permission.

```graphql
query PreRegistrations($updatedSince: DateTime, $pagination: PreRegistrationsPaginationInput) {
  preRegistrations(updatedSince: $updatedSince, pagination: $pagination) {
    __typename
    ... on PreRegistrationsConnection {
      edges {
        node {
          __typename
          id
          product
          responsiblePartnerId
          customerEmail
          customerID
          startedAt
          updatedAt
          ... on PreRegistration {
            origin
            complete
            completedAt
            claimedFromInvitationId
          }
          ... on PreRegistrationInvitation {
            state
          }
        }
      }
      pageInfo {
        hasNextPage
        endCursor
      }
    }
    ... on ErrorInterface {
      message
    }
  }
}
```

| Field | Description |
|-------|-------------|
| `id` | The entry's stable identifier. Use it as the key in your system. For an entry your integration created, it is the `preRegistrationId` or `invitationId` that `createPreRegistration` returned. A `PRE_REGISTRATION_CREATED` webhook carries the same value. |
| `product` | `LIECHTENSTEIN_LIFE_AVD` or `LIECHTENSTEIN_LIFE_AVD_PLUS`. Null when the product cannot be determined from what is stored. |
| `responsiblePartnerId` | Lifeware ID of the broker in your tree the entry is attributed to. For an entry a broker's colleague prepared, this is the broker. |
| `customerEmail` | The email address of the customer's app account, or for an invitation, the address it was sent to. A change to an account's address does not move `updatedAt`. |
| `customerID` | The ID the `customer` query uses. See below before relying on it. |
| `startedAt` | When the entry was created. For a pre-registration claimed from an invitation, this is when the invitation was claimed. |
| `updatedAt` | For a pre-registration, when its stored answers last changed, which can happen without any listed field changing. For an invitation, when its state last changed. |
| `origin` | `BROKER` when a Vermittler wrote it directly into the account, `INVITATION` when the customer claimed an invitation. |
| `complete`, `completedAt` | Whether every question the pre-registration asks has been answered, and when that happened. Details collected at signing, such as tax and social-insurance identifiers, are not part of this. `completedAt` is occasionally null for a complete pre-registration when that moment is not known. |
| `claimedFromInvitationId` | On a pre-registration claimed from an invitation, that invitation's `id`. |
| `state` | `SENT`, `CLAIMED`, `LAPSED` or `CANCELLED`. `LAPSED` covers every way an invitation ends without being claimed; the reason is not disclosed, because it is about the recipient. |

### Do not key entries on `customerID`

On a pre-registration, `customerID` is null until the customer's app account is linked to a customer in the insurance system. For a prospect a Vermittler invited, that can be well after they claimed the invitation, and it can then appear without `updatedAt` changing. On an invitation, it is set only if the person was already a customer in your tree when it was sent. Key entries on `id`, and treat `customerID` as an optional link to the `customer` query.

### Claimed invitations

When an invited person claims an invitation, two entries describe them: the invitation, now in state `CLAIMED`, and their new pre-registration with its own `id`. The pre-registration names the invitation in `claimedFromInvitationId`, so you can merge the two in your system. No webhook fires for the claim: the new pre-registration arrives through the query, and the invitation's `updatedAt` moves to the moment of the claim.

### Keeping your list in sync

Entries are ordered by `id`, 50 per page by default and at most 100. Page forwards by passing `pageInfo.endCursor` as `pagination.after` while `hasNextPage` is `true`.

1. Note the current time, then read the full list.
2. On each later run, note the time again, then page through the query with `updatedSince` set to the previously noted time less a safety margin of a few minutes. The margin covers clock differences and changes still being saved while you read. An entry may then arrive twice; update it in place by `id`.
3. Subscribe a webhook to [`PRE_REGISTRATION_CREATED`](https://partner.life.li/integration-docs/platform-integrations/webhooks#pre_registration_created) to hear when a Vermittler adds an entry between runs.

Entries can leave the list without notice. A pre-registration is deleted when the customer or a Vermittler withdraws it, or when the customer's account is deleted. An invitation can disappear when its email could not be sent, or when the account of the Vermittler who prepared it is removed. A removed entry leaves the list without a webhook and is not returned by an `updatedSince` read. Changes to an account's email address or `customerID` do not move `updatedAt` either. Re-read the full list from time to time to pick these up.

## Errors and retries

Declared errors appear in `data.createPreRegistration` with their own `__typename`. GraphQL input errors, such as an invalid email or multiple `@oneOf` choices, appear in the top-level `errors` field.

| Result | Action |
|--------|--------|
| `UnauthorizedOperationError` | Check integration permissions, the selected broker's access and German market. |
| `CustomerNotFoundError` | Check the identifier and current broker relationship. |
| `PreRegistrationInputValidationError` | Correct the returned `issues`, using each issue's `path`, `code` and `message`. Paths are relative to `liechtensteinLifeAVDPlus`. |
| `PreRegistrationAlreadyExistsError` | Use the existing pre-registration; this mutation cannot replace it. |
| `PreRegistrationInvitationRefusedError` | Handle `reason` as described below. |
| `UnexpectedError` | Contact support, including the reference when supplied, before retrying. A record or invitation may already exist. |

Invitation refusal reasons are `DECLARATION_MISSING`, `PARTNER_DOES_NOT_ALLOW`, `DAILY_LIMIT_REACHED`, `NO_EMAIL_ON_RECORD`, `RECIPIENT_IS_EXISTING_CUSTOMER` and `SEND_FAILED`. Confirm the declaration, resolve the partner restriction, correct the stored email, select the existing customer or investigate the failed send as appropriate. For `DAILY_LIMIT_REACHED`, `retryAt` gives the earliest next attempt; it is `null` for other reasons.

This mutation has no request idempotency key. Retrying a new-customer request can create another invitation. After a timeout or uncertain response, reconcile the outcome with support before sending the request again.

## Relevant schema types

```graphql
directive @oneOf on INPUT_OBJECT | FIELD_DEFINITION

"""
Input for querying an existing customer in the core insurance system.
Only one of the fields can be provided to identify the customer.

**Note**: The customer must be associated with your broker account to be accessible.
"""
input CustomerByInput @oneOf {
  """
  Internal identifier of the customer in the core insurance system.
  This is the most reliable way to identify a customer.
  """
  ID: NonEmptyString
  """
  Email address associated with the customer's account.
  Must match exactly the email used in their existing policies or offers.
  """
  email: Email
}

"""
Error interface for all errors that can be returned by the API.
Always include the `ErrorInterface` in your request so any error returned by the API will be returned.
"""
interface ErrorInterface {
  """A short description of the error."""
  message: String!
}

"""
Error indicating the partner lacks required authorization for the requested operation.

Common causes:
- Missing permissions to sell the specific product
- Missing permission to read policies
- Reading the policies of sub-brokers without entire-tree access
"""
type UnauthorizedOperationError implements ErrorInterface {
  message: String!
}

input CreatePreRegistrationInput @oneOf {
  """Altersvorsorgedepot Plus, for residents of Germany."""
  liechtensteinLifeAVDPlus: CreateLiechtensteinLifeAVDPlusPreRegistrationInput
}

input CreateLiechtensteinLifeAVDPlusPreRegistrationInput {
  customer: AVDPreRegistrationCustomerInput!
  contribution: AVDPreRegistrationContributionInput!
  subsidyEntitlement: AVDPreRegistrationSubsidyEntitlementInput!
  """
  Existing pension contracts. Supply an empty list when the customer has none.
  """
  existingContracts: [AVDPreRegistrationExistingContractInput!]!
  productConfiguration: AVDPreRegistrationProductConfigurationInput!
  """
  Distinct funds with positive whole percentages totalling 100. Maximum fund risk class: 5. Funds
  under Swiss law (ISINs starting with `CH`) and internal strategies (IDs starting with `LLA`) are
  refused, although `availableFunds` lists them for `LIECHTENSTEIN_LIFE_INVEST`.
  """
  fundAllocations: [AVDPreRegistrationFundAllocationInput!]!
  """
  Required when the submission needs an invitation; optional for an existing app account.
  """
  invitationDeclaration: AVDPreRegistrationInvitationDeclarationInput
}

input AVDPreRegistrationCustomerInput @oneOf {
  """
  A prospect outside your customer book. This sends an invitation to the supplied address.
  """
  newCustomer: AVDPreRegistrationPersonInput
  """
  A customer in the acting broker's current customer book or colleague seat,
  identified using the same ID/email as the customer query.
  """
  existingCustomer: AVDPreRegistrationExistingCustomerInput
}

input AVDPreRegistrationExistingCustomerInput {
  by: CustomerByInput!
  """
  Complete application answers, including any details updated during the advice conversation.
  """
  person: AVDPreRegistrationPersonInput!
}

input AVDPreRegistrationPersonInput {
  firstName: NonEmptyString!
  lastName: NonEmptyString!
  dateOfBirth: Date!
  gender: AVDPreRegistrationGender!
  maritalStatus: AVDPreRegistrationMaritalStatus
  nationality: Country
  email: Email!
  phone: PhoneNumber
  address: AVDPreRegistrationAddressInput!
  profession: String
  """
  German tax identification number. Optional until signing; a supplied number must be valid.
  """
  taxId: String
  """Must be explicitly true: German tax residency has been confirmed."""
  taxResidencyDE: Boolean!
  usTaxLiable: Boolean = false
}

input AVDPreRegistrationAddressInput {
  street: NonEmptyString!
  houseNumber: NonEmptyString!
  zip: NonEmptyString!
  city: NonEmptyString!
  country: Country!
}

enum AVDPreRegistrationGender {
  FEMALE
  MALE
  DIVERSE
}

enum AVDPreRegistrationMaritalStatus {
  SINGLE
  MARRIED
  PARTNERSHIP
  DIVORCED
  WIDOWED
}

input AVDPreRegistrationContributionInput {
  """
  Monthly contribution in EUR cents. The same age-dependent minimum as Partner Life applies.
  """
  monthlyContribution: MonetaryFractionalAmount!
  retirementAge: Int!
  careerStarter: Boolean = false
}

input AVDPreRegistrationSubsidyEntitlementInput {
  type: AVDPreRegistrationSubsidyType!
  employmentType: AVDPreRegistrationEmploymentType!
  """
  The Rentenversicherungsnummer. Optional until signing. For direct entitlement, a supplied number
  must be eight digits, a letter and three digits whose last digit is the correct check digit;
  spacing and letter case are ignored.
  """
  socialInsuranceNumber: String
  """
  Required for indirect entitlement, including affirmative non-separation.
  """
  spouse: AVDPreRegistrationSpouseInput
  children: [AVDPreRegistrationChildInput!]!
}

enum AVDPreRegistrationSubsidyType {
  DIRECT
  INDIRECT
}

enum AVDPreRegistrationEmploymentType {
  EMPLOYED
  SELF_EMPLOYED
  PROFESSIONAL_PENSION_SCHEME
  CIVIL_SERVANT
  OTHER
}

input AVDPreRegistrationSpouseInput {
  firstName: NonEmptyString!
  lastName: NonEmptyString!
  dateOfBirth: Date
  taxId: String
  """
  The spouse's Rentenversicherungsnummer. Optional until signing. A supplied number must be eight
  digits, a letter and three digits whose last digit is the correct check digit; spacing and letter
  case are ignored.
  """
  socialInsuranceNumber: String
  notPermanentlySeparated: Boolean!
}

input AVDPreRegistrationChildInput {
  firstName: NonEmptyString!
  birthYear: Int!
  allowanceYears: AVDPreRegistrationChildAllowanceYears!
  receivesChildBenefit: Boolean!
  childBenefitRecipient: AVDPreRegistrationChildBenefitRecipient!
}

enum AVDPreRegistrationChildAllowanceYears {
  UNTIL_18
  UNTIL_25
}

enum AVDPreRegistrationChildBenefitRecipient {
  SELF
  PARTNER
}

input AVDPreRegistrationExistingContractInput {
  kind: AVDPreRegistrationExistingContractKind!
  provider: NonEmptyString!
  contractNumber: NonEmptyString!
  path: AVDPreRegistrationExistingContractPath!
}

enum AVDPreRegistrationExistingContractKind {
  RIESTER
}

enum AVDPreRegistrationExistingContractPath {
  NONE
  LEGAL_FRAMEWORK_CHANGE
  CAPITAL_TRANSFER
}

input AVDPreRegistrationProductConfigurationInput {
  payoutForm: AVDPreRegistrationPayoutForm!
  lumpSumPercent: Int!
  """One of 0, 10 or 20, as in Partner Life."""
  pensionGuaranteeYears: Int!
  annualIncreasePercent: Int!
  """
  Optional until signing. This records bank details only; no SEPA mandate is granted.
  """
  payment: AVDPreRegistrationPaymentInput
}

enum AVDPreRegistrationPayoutForm {
  LIFELONG
  SPLIT_80_20
  PAYOUT_PLAN
}

input AVDPreRegistrationPaymentInput {
  iban: String
  accountHolder: String
}

input AVDPreRegistrationFundAllocationInput {
  isin: NonEmptyString!
  percent: Int!
}

input AVDPreRegistrationInvitationDeclarationInput {
  """
  Must be explicitly true: these answers were collected in conversation with the customer,
  who knows they will receive an invitation by email.
  """
  confirmed: Boolean!
  """Language in which the broker read the declaration."""
  locale: AVDPreRegistrationLocale!
  """Invitation language. Defaults to German, as in Partner Life."""
  mailLocale: AVDPreRegistrationLocale = DE
}

enum AVDPreRegistrationLocale {
  DE
  EN
  FR
  IT
}

union CreatePreRegistrationResult = CreatedPreRegistration | PreRegistrationInvitationSent | PreRegistrationAlreadyExistsError | PreRegistrationInvitationRefusedError | CustomerNotFoundError | PreRegistrationInputValidationError | UnauthorizedOperationError | UnexpectedError

type CreatedPreRegistration {
  preRegistrationId: UUID!
}

type PreRegistrationInvitationSent {
  invitationId: UUID!
}

type PreRegistrationAlreadyExistsError implements ErrorInterface {
  message: String!
}

type PreRegistrationInputValidationError implements ErrorInterface {
  """
  Invalid fields, with paths relative to the `liechtensteinLifeAVDPlus` input.
  """
  issues: [PreRegistrationInputIssue!]!
}

type PreRegistrationInputIssue {
  path: [String!]!
  code: String!
  message: String!
}

type PreRegistrationInvitationRefusedError implements ErrorInterface {
  reason: PreRegistrationInvitationRefusalReason!
  """
  When the broker's invitation limit permits another attempt; otherwise null.
  """
  retryAt: DateTime
}

enum PreRegistrationInvitationRefusalReason {
  DECLARATION_MISSING
  PARTNER_DOES_NOT_ALLOW
  DAILY_LIMIT_REACHED
  NO_EMAIL_ON_RECORD
  RECIPIENT_IS_EXISTING_CUSTOMER
  SEND_FAILED
}

"""
In case an unexpected error occurs during the update customer operation. Please contact customer support for assistance.
"""
type UnexpectedError implements ErrorInterface {
  message: String!
}

"""
Error indicating that the requested customer was not found in the system.

Common causes:
- Customer does not exist in the system
- Invalid customer identifier provided
- On test environment, the customers reset every Thursday and customers created before that date are not available anymore.
"""
type CustomerNotFoundError implements ErrorInterface {
  message: String!
}

"""Cursor pagination, forwards only. Omit it to get the first 50 entries."""
input PreRegistrationsPaginationInput {
  """
  Return the entries after this cursor. Take the value from `edges.cursor` or
  `pageInfo.endCursor` of the previous page.
  """
  after: String
  """How many entries to return. Defaults to 50."""
  first: Int @constraint(min: 1, max: 100)
}

union PreRegistrationsResult = PreRegistrationsConnection | UnauthorizedOperationError | InputValidationError | UnexpectedError

"""
One page of pre-registration entries. `pageInfo.hasPreviousPage` is always `false`, because this
list pages forwards only.
"""
type PreRegistrationsConnection {
  edges: [PreRegistrationEdge!]!
  pageInfo: PageInfo!
}

type PreRegistrationEdge {
  """Pass this value as `after` to read the entries that follow this one."""
  cursor: String!
  node: PreRegistrationEntry!
}

"""
A Vormerkung attributed to your broker tree. Select `__typename` to tell the kinds apart:
`PreRegistration` is held in a customer's app account, and `PreRegistrationInvitation` tracks
an invitation and its outcome.
"""
interface PreRegistrationEntry {
  """
  The entry's stable identifier. Use it as the key in your system. For an entry your integration
  created, it is the `preRegistrationId` or `invitationId` that `createPreRegistration` returned. A
  `PRE_REGISTRATION_CREATED` webhook carries the same value.
  """
  id: ID!
  """
  Who prepared the entry and whether it came through an invitation. This is separate from
  `responsiblePartnerId`, which identifies the broker in your tree it is attributed to.
  """
  origin: PreRegistrationOrigin!
  """Null when the product cannot be determined from what is stored."""
  product: PreRegistrationProduct
  """
  The Lifeware ID of the broker in your tree the entry is attributed to. For an entry a broker's
  colleague prepared, this is the broker.
  """
  responsiblePartnerId: String!
  """
  For a pre-registration, the email address of the customer's app account. A change to that
  address does not move `updatedAt`. For an invitation, the address it was sent to.
  """
  customerEmail: String!
  """
  The customer's ID, as the `customer` query uses it. Do not use it to identify an entry.
  
  For a pre-registration, null until the customer's app account is linked to a customer in the
  insurance system, which for an invited prospect can be well after they claimed the invitation.
  It can then appear without `updatedAt` changing. For an invitation, set only if the person was
  already a customer in your tree when it was sent.
  """
  customerID: UUID
  """
  When the entry was created. For a pre-registration claimed from an invitation, this is when the
  invitation was claimed; the invitation's own `startedAt` is when it was sent.
  """
  startedAt: DateTime!
  """
  For a pre-registration, the later of its last answer update and its last broker fund selection.
  This can change without any other field here changing. For an invitation, when its state last
  changed.
  """
  updatedAt: DateTime!
}

"""
A pre-registration held in a customer's app account. The customer can see and change it in the app.
"""
type PreRegistration implements PreRegistrationEntry {
  id: ID!
  """
  Whether every question the pre-registration asks has been answered. Details collected at
  signing, such as tax and social-insurance identifiers, are not part of this.
  """
  complete: Boolean!
  """
  Whether this is an Altersvorsorgedepot Plus pre-registration with no funds assigned yet.
  Independent of `complete`: the customer's questions can be complete while funds still need
  to be selected by a Vermittler. False for the standard depot and when stored answers cannot
  be read safely (`product` is then null).
  """
  fundsMissing: Boolean!
  """
  When the pre-registration became complete. Null while it is incomplete, and occasionally null
  for a complete pre-registration when that moment is not known.
  """
  completedAt: DateTime
  """
  The invitation this pre-registration was claimed from. Null when `origin` is `BROKER` or
  `CUSTOMER`, and when that invitation is no longer listed.
  """
  claimedFromInvitationId: ID
}

"""
An invitation to someone who has no app account yet, or is not in your tree. It holds the
Vermittler's answers until the person registers and claims them.
"""
type PreRegistrationInvitation implements PreRegistrationEntry {
  id: ID!
  state: PreRegistrationInvitationState!
}

enum PreRegistrationProduct {
  """Liechtenstein Life Altersvorsorgedepot, with the standard depot."""
  LIECHTENSTEIN_LIFE_AVD
  """
  Liechtenstein Life Altersvorsorgedepot Plus, with funds selected from the Liechtenstein Life range.
  """
  LIECHTENSTEIN_LIFE_AVD_PLUS
}

enum PreRegistrationOrigin {
  """
  The customer filled it in themselves, independently or through a copied broker link.
  """
  CUSTOMER
  """A Vermittler wrote it directly into the customer's app account."""
  BROKER
  """
  A Vermittler prepared it as an invitation. On `PreRegistration`, the customer has claimed it;
  on `PreRegistrationInvitation`, use `state` to see whether it has been claimed.
  """
  INVITATION
}

enum PreRegistrationInvitationState {
  """Sent, and waiting for the person to register."""
  SENT
  """
  The person registered and claimed it. Their pre-registration is listed as its own entry.
  """
  CLAIMED
  """
  It ended without being claimed. We do not say why, because the reason is about the recipient.
  """
  LAPSED
  """A Vermittler cancelled it."""
  CANCELLED
}
```

Source: https://partner.life.li/integration-docs/platform-integrations/pre-registrations
Use the deployed API schema for shared scalar definitions and the current broker's fund catalogue.

Access and broker attribution​

Use the authentication described in Platform GraphQL API. Every request needs your API key and an x-partner-id header containing the acting broker's Lifeware ID. Your key must be authorized to act for that broker.

Your integration needs CREATE_OFFER_V2. The responsible broker must have new-business access and the German market. A colleague acts under their parent broker: the colleague remains the author, while the responsible broker receives the attribution and uses their invitation allowance. The API resolves these relationships; neither a broker ID nor an app user ID belongs in the input.

Pensionfriend and Nettowelt already have the required API permission. Pensionfriend can act for its configured broker IDs; Nettowelt can also act for brokers in its configured broker trees. Both remain subject to the broker and customer access checks above.

The integration's access is checked against the current hierarchy. A colleague with an individual seat can select only customers visible to that seat; a back-office colleague can act for the broker's customer book.

Complete request​

Always request __typename and ErrorInterface.message, then handle the two success types separately.

mutation CreatePreRegistration($input: CreatePreRegistrationInput!) {
createPreRegistration(input: $input) {
__typename
... on CreatedPreRegistration {
preRegistrationId
}
... on PreRegistrationInvitationSent {
invitationId
}
... on ErrorInterface {
message
}
... on PreRegistrationInputValidationError {
issues {
path
code
message
}
}
... on PreRegistrationInvitationRefusedError {
reason
retryAt
}
}
}

The variables below show a new customer with direct subsidy entitlement, no children and no existing pension contracts. Replace the placeholder ISIN with an eligible fund before sending the request. Altersvorsorgedepot Plus takes its funds from the Liechtenstein Life Invest range that availableFunds(product: LIECHTENSTEIN_LIFE_INVEST) returns, within the limits listed below the example.

{
"input": {
"liechtensteinLifeAVDPlus": {
"customer": {
"newCustomer": {
"firstName": "Alex",
"lastName": "Example",
"dateOfBirth": "1990-05-17",
"gender": "DIVERSE",
"email": "alex@example.com",
"address": {
"street": "Example Street",
"houseNumber": "1",
"zip": "10115",
"city": "Berlin",
"country": "DE"
},
"taxResidencyDE": true
}
},
"contribution": {
"monthlyContribution": 20000,
"retirementAge": 67
},
"subsidyEntitlement": {
"type": "DIRECT",
"employmentType": "EMPLOYED",
"children": []
},
"existingContracts": [],
"productConfiguration": {
"payoutForm": "LIFELONG",
"lumpSumPercent": 0,
"pensionGuaranteeYears": 0,
"annualIncreasePercent": 0
},
"fundAllocations": [
{
"isin": "REPLACE_WITH_ELIGIBLE_FUND_ISIN",
"percent": 100
}
],
"invitationDeclaration": {
"confirmed": true,
"locale": "EN",
"mailLocale": "EN"
}
}
}
}

monthlyContribution is in EUR cents: 20000 means EUR 200 per month. Partner Life's age-dependent contribution minimum and retirement-age rules also apply here. Fund ISINs must be distinct; percentages must be positive whole numbers totalling 100. Each fund must have a known risk class from 1 to 5. Funds under Swiss law (ISINs starting with CH) and Liechtenstein Life's internal strategies (IDs starting with LLA) are not admitted, although availableFunds lists them for Liechtenstein Life Invest. A refused fund is reported under the issue code fundRiskAboveLimit, whatever the reason. The placeholder above is not a usable fund selection.

taxResidencyDE must be explicitly true, and the residence country must be DE. For indirect entitlement, supply the spouse's required details and notPermanentlySeparated: true. Supply children: [] and existingContracts: [] when those lists are empty.

Existing customers​

Replace customer.newCustomer with customer.existingCustomer. Set by to either the customer ID returned by the customer query or their email, and put the complete person answers under person:

{
"existingCustomer": {
"by": {
"ID": "00000000-0000-4000-8000-000000000001"
},
"person": {
"firstName": "Alex",
"lastName": "Example",
"dateOfBirth": "1990-05-17",
"gender": "DIVERSE",
"email": "alex@example.com",
"address": {
"street": "Example Street",
"houseNumber": "1",
"zip": "10115",
"city": "Berlin",
"country": "DE"
},
"taxResidencyDE": true
}
}
}

Use the real customer ID in place of the example UUID. To identify them by email, use "by": { "email": "alex@example.com" } instead. Supply exactly one identifier and exactly one customer choice.

When several customers in scope share an email, the API returns an ambiguousCustomer validation issue. Select the person by ID. Existing customers must be natural persons; companies cannot hold a personal pension pre-registration.

The customer must belong to your broker subtree through a current policy, offer or eligible direct broker assignment from createCustomer. The API resolves account ownership again before writing. If they have no account, the invitation goes to their email on record, even if person.email contains a different answer. This operation does not update that stored email.

Selecting an existing customer outside the acting broker's book or colleague seat returns CustomerNotFoundError, whether identified by ID or email. The request creates no pre-registration or invitation and sends no email. Authorization to act for several brokers does not combine their customer books: each request uses the scope of its x-partner-id.

Use existingCustomer for a customer already in your book. A new-customer request naming an email found in your own customer search is refused with RECIPIENT_IS_EXISTING_CUSTOMER. An email outside your book receives no account-ownership disclosure.

Register a customer, then continue in Partner Life​

You can instead register the customer using createCustomer and keep its returned customerID. The customer is assigned to the broker selected by x-partner-id and can be selected in Partner Life's AVD customer search before an offer or contract exists, subject to the same current broker and colleague access rules.

The broker can then prepare the pre-registration in Partner Life, including the fund selection. Customer registration alone creates no AVD pre-registration, app account or offer and sends no AVD invitation; the pre-registration submission triggers the applicable preparation email or invitation.

Invitations and deferred details​

Include invitationDeclaration whenever the recipient needs an invitation. Set confirmed: true only when the broker collected these answers in conversation with the customer and the customer knows they will receive an invitation by email. locale records the declaration's language; mailLocale selects the invitation language and defaults to DE. Supported values are DE, EN, FR and IT. An existing customer with an app account can omit this declaration.

Invitations retain the submitted answers while the recipient confirms their identity. They expire after 14 days and share Partner Life's limit of 20 creations per responsible broker in a rolling 24-hour window. Partner and recipient eligibility restrictions continue to apply. Treat PreRegistrationInvitationSent as an accepted invitation: it does not confirm mailbox delivery or disclose the recipient's account status.

Tax and social-insurance identifiers and bank details can be deferred until signing; supplied identifiers must pass the applicable validation. Bank details grant no SEPA mandate. Final legal acknowledgments and transfer mandates are collected later from the customer, not accepted through this pre-registration API.

Listing pre-registrations​

Use the preRegistrations query to read back the Vormerkungen a Vermittler prepared in the broker's tree, whether in Partner Life or through createPreRegistration. Entries a colleague prepared before leaving stay listed. It lists these kinds of entry:

  • PreRegistration: held in a customer's app account. The customer can see and change it in the app.
  • PreRegistrationInvitation: an invitation waiting for the person to register and claim the answers.

As in Partner Life, pre-registrations a customer filled in themselves are listed too, complete or incomplete, without a copied broker link or its sharing consent. A pre-registration another tree's Vermittler prepared for a customer who is also attributed to your tree is listed, as Partner Life shows it to you too. Your integration needs the PRE_REGISTRATION_READ permission. The broker in x-partner-id needs the same broker access as for createPreRegistration. Pensionfriend has this permission.

query PreRegistrations($updatedSince: DateTime, $pagination: PreRegistrationsPaginationInput) {
preRegistrations(updatedSince: $updatedSince, pagination: $pagination) {
__typename
... on PreRegistrationsConnection {
edges {
node {
__typename
id
product
responsiblePartnerId
customerEmail
customerID
startedAt
updatedAt
... on PreRegistration {
origin
complete
completedAt
claimedFromInvitationId
}
... on PreRegistrationInvitation {
state
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
... on ErrorInterface {
message
}
}
}
FieldDescription
idThe entry's stable identifier. Use it as the key in your system. For an entry your integration created, it is the preRegistrationId or invitationId that createPreRegistration returned. A PRE_REGISTRATION_CREATED webhook carries the same value.
productLIECHTENSTEIN_LIFE_AVD or LIECHTENSTEIN_LIFE_AVD_PLUS. Null when the product cannot be determined from what is stored.
responsiblePartnerIdLifeware ID of the broker in your tree the entry is attributed to. For an entry a broker's colleague prepared, this is the broker.
customerEmailThe email address of the customer's app account, or for an invitation, the address it was sent to. A change to an account's address does not move updatedAt.
customerIDThe ID the customer query uses. See below before relying on it.
startedAtWhen the entry was created. For a pre-registration claimed from an invitation, this is when the invitation was claimed.
updatedAtFor a pre-registration, when its stored answers last changed, which can happen without any listed field changing. For an invitation, when its state last changed.
originBROKER when a Vermittler wrote it directly into the account, INVITATION when the customer claimed an invitation.
complete, completedAtWhether every question the pre-registration asks has been answered, and when that happened. Details collected at signing, such as tax and social-insurance identifiers, are not part of this. completedAt is occasionally null for a complete pre-registration when that moment is not known.
claimedFromInvitationIdOn a pre-registration claimed from an invitation, that invitation's id.
stateSENT, CLAIMED, LAPSED or CANCELLED. LAPSED covers every way an invitation ends without being claimed; the reason is not disclosed, because it is about the recipient.

Do not key entries on customerID​

On a pre-registration, customerID is null until the customer's app account is linked to a customer in the insurance system. For a prospect a Vermittler invited, that can be well after they claimed the invitation, and it can then appear without updatedAt changing. On an invitation, it is set only if the person was already a customer in your tree when it was sent. Key entries on id, and treat customerID as an optional link to the customer query.

Claimed invitations​

When an invited person claims an invitation, two entries describe them: the invitation, now in state CLAIMED, and their new pre-registration with its own id. The pre-registration names the invitation in claimedFromInvitationId, so you can merge the two in your system. No webhook fires for the claim: the new pre-registration arrives through the query, and the invitation's updatedAt moves to the moment of the claim.

Keeping your list in sync​

Entries are ordered by id, 50 per page by default and at most 100. Page forwards by passing pageInfo.endCursor as pagination.after while hasNextPage is true.

  1. Note the current time, then read the full list.
  2. On each later run, note the time again, then page through the query with updatedSince set to the previously noted time less a safety margin of a few minutes. The margin covers clock differences and changes still being saved while you read. An entry may then arrive twice; update it in place by id.
  3. Subscribe a webhook to PRE_REGISTRATION_CREATED to hear when a Vermittler adds an entry between runs.

Entries can leave the list without notice. A pre-registration is deleted when the customer or a Vermittler withdraws it, or when the customer's account is deleted. An invitation can disappear when its email could not be sent, or when the account of the Vermittler who prepared it is removed. A removed entry leaves the list without a webhook and is not returned by an updatedSince read. Changes to an account's email address or customerID do not move updatedAt either. Re-read the full list from time to time to pick these up.

Errors and retries​

Declared errors appear in data.createPreRegistration with their own __typename. GraphQL input errors, such as an invalid email or multiple @oneOf choices, appear in the top-level errors field.

ResultAction
UnauthorizedOperationErrorCheck integration permissions, the selected broker's access and German market.
CustomerNotFoundErrorCheck the identifier and current broker relationship.
PreRegistrationInputValidationErrorCorrect the returned issues, using each issue's path, code and message. Paths are relative to liechtensteinLifeAVDPlus.
PreRegistrationAlreadyExistsErrorUse the existing pre-registration; this mutation cannot replace it.
PreRegistrationInvitationRefusedErrorHandle reason as described below.
UnexpectedErrorContact support, including the reference when supplied, before retrying. A record or invitation may already exist.

Invitation refusal reasons are DECLARATION_MISSING, PARTNER_DOES_NOT_ALLOW, DAILY_LIMIT_REACHED, NO_EMAIL_ON_RECORD, RECIPIENT_IS_EXISTING_CUSTOMER and SEND_FAILED. Confirm the declaration, resolve the partner restriction, correct the stored email, select the existing customer or investigate the failed send as appropriate. For DAILY_LIMIT_REACHED, retryAt gives the earliest next attempt; it is null for other reasons.

This mutation has no request idempotency key. Retrying a new-customer request can create another invitation. After a timeout or uncertain response, reconcile the outcome with support before sending the request again.