# User and account opening

This guide walks you through the process of creating a German pension offering including onboarding users, creating account groups and accounts, creating the tax wrapper, and setting up the allowance.

## Prerequisites

* **Authentication scopes** - `pensions:admin`
* **Webhook handler ready** — Set up a webhook endpoint to listen for events relating to pension account opening
  * Subscribe to `USERS.*` events.
  * Subscribe to `ACCOUNT_GROUP.*` events.
  * Subscribe to `PENSION_DE_WRAPPER.*` events.
  * Subscribe to `ACCOUNT.*` events.
  * Subscribe to `PENSION_DE_WRAPPER_ALLOWANCE.*` events.


For more information, refer to [Implementing webhooks](/products/byol/getting_started/implementing_webhooks).

Before opening an Altersvorsorgedepot and/or Standarddepot, an end user must be created in the Upvest system. For existing eligible end users, Altersvorsorgedepot and/or Standarddepot accounts can be opened directly after adding additional data to the user model as outlined below.

## Enabling pension accounts via tax wrapper functionality

### 1. Onboarding: Creating new and updating existing users

Adding a user follows the usual user creation flow (see the [Users guide](/products/byol/guides/users) for more information). This includes verifying that the user is a tax resident of Germany. You can use the `POST /users/{user_id}/tax_residencies` endpoint to create the end user’s residency.

In addition, there are a series of fields that are required for both the Altersvorsorgedepot and the Standarddepot when applying for the government bonus. You can add the additional fields when completing the initial user onboarding or when updating existing users details.

If an end user provides these details after onboarding, or for other changes to user data, you can update the details for an existing user by submitting a user data change via the `POST /users/{user_id}/data_change_requests` endpoint as shown below.

When requesting user data changes, submit each request individually and wait for the `/user events` webhook confirmation before submitting the next request.

#### Additional fields required for the bonus application

**`POST /users/{user_id}/data_change_requests`**

Gender
```json
{
"gender": "MALE"
}
```

Birth city
```json
{
"birth_city": "Berlin"
}
```

Social security number
```json
{
"social_security_number": "25300972S014"
}
```

Birth name
```json
{
"birth_name": "Meyer"
}
```

The `birth_name` field is required when the `last_name` is different.

### 2. Onboarding: Creating the Altersvorsorgedepot and Standarddepot account groups

After creating or updating the user, the next step is the creation of an account group. You must create a separate account group for each contract. The account group for Altersvorsorgedepot and Standarddepot is PENSION_DE in both cases.

Set `type` equal to `PENSION_DE` when creating the account group:

#### Example creating a German pension account group

**`POST /account_groups`**

Request
```json
{
  "user_id": "413715f2-5401-4b97-8055-034a6b879f8c",
  "type": "PENSION_DE"
}
```

Response
```json
{
  "id": "9d95820d-4333-46b6-98de-04ab7512e76f",
  "created_at": "2027-08-24T14:15:22Z",
  "updated_at": "2027-08-24T14:15:22Z",
  "type": "PENSION_DE",
  "status": "ACTIVE",
  "users": [
    {
      "id": "413715f2-5401-4b97-8055-034a6b879f8c",
      "type": "OWNER"
    }
  ],
  "securities_account_number": "123456789"
}
```

After creating the account group, you can proceed with creating the tax wrapper and accounts.

**Maximum two pension account rule**

End users can hold a maximum of two pension accounts at any time. Creating a third pension account will be restricted.

### 3. Create the tax wrapper

The business rules and logic applicable to a tax-wrapped investment product are managed through the tax wrapper entity. The tax wrapper represents the entirety of the Altersvorsorgedepot or Standarddepot contract.

When creating the tax wrapper the `type` - either Altersvorsorgedepot (`AVD`) or `STANDARDDEPOT` - must be specified for each pension account. A dedicated API call is available for each tax wrapper type.

#### Contract number

The contract number is required to enable Upvest to register the account with the ZfA, process bonus applications and transfers. Clients can either provide their own contract number or Upvest can generate the contract number. Contract numbers must be unique, any repeated contract numbers will be rejected. When tenant configuration is set for Upvest to create the numbers, they are automatically provided in the tax wrapper creation response.

#### Expected payout age

The expected payout age informs the payout start date on the payout configuration, which is automatically created on tax wrapper creation. Any further changes to payout start date can be done directly in the payout configuration.

You can update the fields related to the payout phase as described in the [Payout phase](/products/byol/guides/tax_wrappers/german_pensions_payout) section of this guide.

#### Terms and conditions

The end user must accept the terms and conditions. This requires setting the `confirmed_at` timestamp for the terms and conditions. This value can be set by:

* The client provides the `confirmed_at` timestamp in the `POST` request when creating the tax wrapper.
* The client adds the `confirmed_at` timestamp in a `PATCH` request anytime before the tax wrapper is activated.
* Upvest automatically adds the `confirmed_at` timestamp and sets it equal to when the tax wrapper is activated.


Once confirmed and the tax wrapper is active, we send the details of the contract to the ZfA and the `confirmed_at` timestamp can no longer be updated.

The `confirmed_at` date informs the contract start date registered with the ZfA and is used to determine eligibility if more than two contracts are registered to a user.

##### Example creating a tax wrapper

**`POST /pension_de/wrappers`**

Request
```json
{
  "type": "AVD",
  "account_group_id": "0d68fea2-66e8-4ea8-b507-276e7a1eb4aa",
  "expected_payout_age": 67,
  "terms_and_conditions": {
    "confirmed_at": ""
  },
  "contract_number": "XJ49-BN22-9901-PQ88",
  "product_information_sheet_id": "a3f9c1d7-82b4-4e6f-9d23-5c0e7f8a1b2d"
}
```

Response
```json
{
  "id": "019999a3-c02a-7043-b100-9c2d76564748",
  "created_at": "2027-05-22T10:00:00.000Z",
  "updated_at": "2027-05-22T10:00:00.000Z",
  "expected_payout_age": 67,
  "terms_and_conditions": {
    "confirmed_at": ""
  },
  "contract_number": "XJ49-BN22-9901-PQ88",
  "type": "AVD",
  "product_information_sheet_id": "a3f9c1d7-82b4-4e6f-9d23-5c0e7f8a1b2d"
}
```

When configuring the tax wrapper, you can set `type` to either `AVD` or `STANDARDDEPOT` depending on the use case.

##### Example updating a tax wrapper

**`PATCH /pension_de/wrappers/{tax_wrapper_id}`**

Request
```json
{
  "terms_and_conditions": {
    "confirmed_at": "2027-07-21T14:10:00.00Z"
  },
  "expected_payout_age": 67,
  "product_information_sheet_id": "f7d2e0a9-3c61-4b8e-a0f5-9d4c8e2b7a16"
}
```

Response
```json
{
  "id": "019999a3-c02a-7043-b100-9c2d76564748",
  "created_at": "2027-05-22T10:00:00.000Z",
  "updated_at": "2027-05-22T10:00:00.000Z",
  "tax_wrapper_id": "019999a3-c02a-7043-b100-9c2d76564748",
  "expected_payout_age": 67,
  "terms_and_conditions": {
    "confirmed_at": "2027-07-21T14:10:00.00Z"
  },
  "type": "AVD",
  "product_information_sheet_id": "f7d2e0a9-3c61-4b8e-a0f5-9d4c8e2b7a16"
}
```

| **Field** | **Description** |
|  --- | --- |
| `type` | The type of pension account. This field accepts `AVD` or `STANDARDDEPOT`. |
| `account_group_id` | The UUID generated when creating the account group. |
| `expected_payout_age` | The age at which the end user would like to move to the payout phase. Clients can choose to default to an age set out in the contract or allow the end user to select.  For new users, this value must be between 65-70. This value cannot be updated once the tax wrapper is created. Instead, you can update the payout date in the payout configuration as described in the [Payout phase](/products/byol/guides/tax_wrappers/german_pensions_payout) section of this guide. |
| `contract_number` | The specific identifier for the Altersvorsorgedepot or Standarddepot. For details on correctly setting this value, refer to the [Contract number](/products/byol/guides/tax_wrappers/german_pensions_implementing#contract-number) section above. |
| `terms_and_conditions``.consent_document_id .confirmed_at` | The unique identifier to record the acceptance of the terms and conditions and the timestamp for acceptance. For details on correctly setting this value, refer to the [Terms and conditions](/products/byol/guides/tax_wrappers/german_pensions_implementing#terms-and-conditions) section above. |
| `tax_wrapper_id` | The unique identifier for the tax wrapper. |
| `product_information_sheet_id` | Set this value equal to the ID of the [product information sheet](/products/byol/guides/tax_wrappers/german_pensions_product_information_sheet) (*Produktinformationsblatt*) associated with this tax wrapper. |


After a tax wrapper is created, it remains inactive until all activation requirements are fulfilled:

* The user must be in `ACTIVE` status, have a valid Tax Identification Number (TIN), and complete a [tax residency assessment](/products/byol/guides/users/users_tax_onboarding).
* The user must review and confirm all pre-contractual disclosures, including the product information sheet, and formally accept the pension contract.


The tax wrapper status changes to `ACTIVE` once these conditions are satisfied. Following tax wrapper activation, the account group and accounts are automatically activated and ready to receive contributions.

### 4. Create the account

Create an account as described in the [Create Accounts](/products/byol/guides/accounts/accounts_create_accounts) guide.

##### Example request to create an account

**`POST /accounts`**

Request
```json
{
  "user_id": "413715f2-5401-4b97-8055-034a6b879f8c",
  "account_group_id": "0d68fea2-66e8-4ea8-b507-276e7a1eb4aa",
  "type": "PORTFOLIO",
  "name": "Main account"
}
```

Response
```json
{
  "id": "ad45b27f-c0e7-4cfb-b48c-a83670dbdfbd",
  "created_at": "2020-08-24T14:15:22Z",
  "updated_at": "2020-08-24T14:15:22Z",
  "account_group_id": "0d68fea2-66e8-4ea8-b507-276e7a1eb4aa",
  "type": "PORTFOLIO",
  "users": [
    {
      "id": "413715f2-5401-4b97-8055-034a6b879f8c",
      "type": "OWNER"
    }
  ],
  "account_number": 1,
  "name": "Main account",
  "status": "ACTIVE"
}
```

When creating the account, you can set `type` to either `TRADING` or `PORTFOLIO` depending on the use case:

### 5. Choosing a trading or portfolio account

Either account type (`PORTFOLIO` or `TRADING`) can be used to support both Standarddepot and Altersvorsorgedepot. For the Standarddepot, it is recommended to use portfolio accounts to simplify the rebalancing requirements when approaching the payout age.

By using a portfolio account you can additionally benefit from the following Upvest functionality:

* Creating propositions with [portfolio allocations](/products/byol/guides/portfolios/enabling/portfolio_allocations#portfolio-allocations) for Standarddepot and discretionarily managed Altersvorsorgedepot.
* Allowing the end user to create their customised portfolio allocation in Standarddepot and self-directed Altersvorsorgedepot.
* Automate order weighting with [portfolio orders.](/products/byol/guides/portfolios/enabling/portfolio_order)
* Utilise self-balancing orders to minimise portfolio drift and reduce the need for rebalancings.
* Allowing users to [rebalance](/products/byol/guides/portfolios/rebalancing/rebalancing_triggering) their accounts.


With the Standarddepot, leveraging portfolios provides the following benefits when approaching the payout phase:

* Switching the [portfolio allocation](/products/byol/guides/portfolios/enabling/switching_allocations) to the prescribed weighting.
* Setting the new allocation in advance of the mandated rebalance period without triggering an immediate rebalance, enabling a glide path to reduce the impact of a single rebalancing event.
* Utilising self-balancing orders so new contributions work towards the (new) target allocation.
* Completing the mandated [rebalance](/products/byol/guides/portfolios/rebalancing/rebalancing_triggering) at both 5 years, and 2 years, prior to the payout phase if not achieved already with the glide path.


### 6. Allowance events

Once the tax wrapper has been successfully created and activated, the Investment API will automatically create the underlying Allowance resource for the current tax year and confirm its creation via the `PENSION_DE_WRAPPER` and `PENSION_DE_WRAPPER_ALLOWANCE` webhooks.

The allowance entity tracks all user contributions to help track the minimum amount for the bonus application and prevents contributions exceeding the maximum contribution amount allowed per tax year. Once the contribution amount reaches the limit, additional contributions are automatically rejected. See the [Blocking contributions beyond the allowance](/products/byol/guides/tax_wrappers/german_pensions_implementing#7-blocking-contributions-beyond-the-allowance) section for details.

Since users are not allowed to carry over remaining allowance amounts from prior years, the Investment API automatically closes the completed year and creates a new allowance each year.

You can view a user’s allowance using the `GET /pension_de/wrappers/{tax_wrapper_id}/allowances/` endpoint. This provides access to the amount of allowance available to the user.

##### Example response for displaying allowances

**`GET /pension_de/wrappers/{tax_wrapper_id}/allowances/`**

Response
```json
{
 "meta": {
   "offset": 0,
   "limit": 100,
   "count": 2,
   "total_count": 2,
   "sort": "valid_from",
   "order": "DESC"
 },
 "data": [
   {
     "allowance_id": "019999a3-8a17-74e6-bfbd-50d9c7705ea0",
     "tax_wrapper_id": "019999a3-c02a-7043-b100-9c2d76564748",
     "tax_year": "2028",
     "type": "ANNUAL",
     "status": "ACTIVE",
     "currency": "EUR",
     "total_amount": "6840.00",
     "used_amount": "0.00",
     "remaining_amount": "6840.00",
     "created_at": "2027-07-21T14:10:00.00Z",
     "updated_at": "2027-07-21T14:10:00.00Z",
     "valid_from": "2028-01-21T14:10:00.00Z",
     "valid_to": "2028-12-31T23:59:59.00Z"
   },
   {
     "allowance_id": "019999a3-8a17-74e6-bfbd-50d9c7705ea0",
     "tax_wrapper_id": "019999a3-c02a-7043-b100-9c2d76564748",
     "tax_year": "2027",
     "type": "ANNUAL",
     "status": "EXPIRED",
     "currency": "EUR",
     "used_amount": "0.00",
     "remaining_amount": "6840.00",
     "created_at": "2027-07-21T14:10:00.00Z",
     "updated_at": "2027-07-21T14:10:00.00Z",
     "valid_from": "2027-07-21T14:10:00.00Z",
     "valid_to": "2027-12-31T23:59:59.00Z"
   }
 ]
}
```

| **Field** | **Description** |
|  --- | --- |
| `allowance_id` | The UUID that is created for each annual allowance. |
| `tax_year` | The year of the allowance. |
| `type` | The `ANNUAL` type refers to how the regulatory contribution limit renews annually. Regulatory changes to this limit are managed by Upvest and will automatically be applied before the start of the tax year. |
| `status` | This field may contain the following values: - `ACTIVE`: the allowance is fully enabled and the user can make additional contributions up to the value of the **remaining_amount**. - `EXPIRED`: the allowance is outside of the `valid_from` and `valid_to` dates. The end user can no longer make contributions against this allowance. Once an allowance entity with the type `ANNUAL` hits the `valid_to` date, it will transition to the status `EXPIRED`. Expired allowances do not roll over. New allowance entities will be created at commencement of the new tax year. |
| **Allowance values** |  |
| `used_amount` | The amount of contributions submitted so far. |
| `remaining_amount` | The calculated difference between the annual allowance and the `used_amount`, indicating how much remaining allowance is available. |
| `valid_from` and `valid_to` | Shows the period in which the allowance is in effect. Normally, these values equal the first and last date of the year. |


### 7. Blocking contributions beyond the allowance

Each end user is allowed to pay-in a maximum of €6,840 per account. The [allowance entity](/products/byol/guides/tax_wrappers/german_pensions_implementing#6-allowance-events) tracks the used amount for each contract and automatically rejects contributions that would take the account beyond the allowable maximum amount. The allowance entity rejects the full contribution amount if the contribution would cause the allowance to exceed the maximum amount.

For example: if an end user submits a contribution of €100 when the `remaining_amount` equals €80, the whole €100 contribution will be rejected. In this case the client can retry with the correct contribution amount of €80.

This works differently depending on whether your cash configuration setup is either [post-trade settlement](/products/byol/guides/tax_wrappers/german_pensions_implementing#post-trade-settlement) or [pre-funding](/products/byol/guides/tax_wrappers/german_pensions_implementing#pre-funding).

#### Post-trade settlement

In this setup, the client retains control of cash payments into their pension pooling account. It is important to proactively check there is sufficient `remaining_amount` before accepting a payment via the `PENSION_DE_WRAPPER_ALLOWANCE` webhook or the `GET /pension_de/wrappers/{tax_wrapper_id}/allowances/` endpoint.

As a fail safe, Upvest also checks all virtual cash increases against the `remaining_amount`. If a contribution results in an ineligible contribution, Upvest rejects the virtual cash increase. This blocks the order from being processed and the client will be notified why the contribution was rejected.

To rectify, the client can reduce the virtual cash payment and return the payment to the end user. The allowance automatically updates.

#### Pre-funding

In this setup Upvest is able to check the payment instruction directly against the allowance. Based on the `remaining_amount` Upvest either:

* accepts the contribution and updates the allowance or,
* rejects the payment; in case of SEPA Credit Funding, the payment is returned to the end user, while SEPA Direct Debit are cancelled before settlement.


The `PENSION_DE_WRAPPER_ALLOWANCE` webhook confirms the action and includes the rejection reason.

Once the payment has been accepted the order can then be placed. For savings plans, this occurs automatically.