# Quickstart

> Credentials to a placed share-purchase order in eight calls, copy-pasteable end to end.

Source: https://docs.valueverde.de/docs/quickstart

# Quickstart

Eight calls, in order, from a `client_id` to a submitted order. Every step
captures what the next one needs, so you can paste the whole page into a shell
and watch it run.

Run it against **staging** — the same contract as production, without moving
real money. When it works, change `VV_HOST` to `https://api.valueverde.de` and
swap in your production credentials; nothing else about these calls changes.

```bash
export VV_HOST="https://api.staging.valueverde.de"
export VV_CLIENT_ID="…"        # your staging credentials, from partner support
export VV_CLIENT_SECRET="…"
```

:::tip
Every example uses `jq` to pull the one field the next step needs. If you do not
have it, read the value out of the response by hand — the field names are the
same.
:::

## 1. Get a token

```bash
export VV_TOKEN=$(curl -sS -X POST "$VV_HOST/oauth2/token" \
  -u "$VV_CLIENT_ID:$VV_CLIENT_SECRET" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" | jq -r .access_token)

echo "${VV_TOKEN:0:24}…"
```

The token lasts **15 minutes**. Cache it and re-mint on expiry; do not fetch one
per request. Details in [Authentication](./authentication).

## 2. Check what your credential can do

```bash
curl -sS "$VV_HOST/v1/me" -H "Authorization: Bearer $VV_TOKEN"
```

```json
{
  "client_id": "01a00233-0335-7ff4-b25f-8837fec2f7f6",
  "organization_id": "01a00233-0330-74f3-9ad2-f56f49c2c940",
  "token_scopes": ["cooperatives:read", "projects:read", "investors:read",
                   "investors:write", "applications:read", "applications:write",
                   "portfolio:read"],
  "granted_scopes": ["cooperatives:read", "projects:read", "investors:read",
                     "investors:write", "applications:read", "applications:write",
                     "portfolio:read"]
}
```

This endpoint needs no scope, so it works with your very first token. If
`granted_scopes` is missing the `investors:*` entries, stop here — steps 4
onwards will `403`, and partner support has to widen the grant.

## 3. Find a cooperative

```bash
export VV_COOP_ID=$(curl -sS "$VV_HOST/v1/cooperatives?page=1&size=1" \
  -H "Authorization: Bearer $VV_TOKEN" | jq -r '.items[0].id')

curl -sS "$VV_HOST/v1/cooperatives/$VV_COOP_ID?expand=projects" \
  -H "Authorization: Bearer $VV_TOKEN" | jq '{id, name, share_price, minimum_shares}'
```

```json
{
  "id": "11111111-1111-4111-8111-111111111111",
  "name": "Buergerenergie Musterhausen eG",
  "share_price": 100,
  "minimum_shares": 1
}
```

`share_price` and `minimum_shares` are what you show the user. You never send an
amount back — see [step 6](#6-place-the-order).

## 4. Register the investor

One call creates the person and fills in their profile. Everything the platform
can derive, it derives.

```bash
export VV_INVESTOR_ID=$(curl -sS -X POST "$VV_HOST/v1/investors" \
  -H "Authorization: Bearer $VV_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "partner_customer_ref": "your-customer-4711",
    "email": "maria@example.org",
    "profile": {
      "first_name": "Maria",
      "last_name": "Santos",
      "birth_date": "1985-03-15",
      "address": {
        "street": "Hauptstrasse",
        "house_number": "1",
        "postal_code": "10115",
        "city": "Berlin"
      },
      "bank_account": { "iban": "DE89370400440532013000" },
      "consents": {
        "terms":        { "given": true },
        "privacy":      { "given": true },
        "data_sharing": { "given": true }
      }
    }
  }' | jq -r .investor_id)

echo "$VV_INVESTOR_ID"
```

That is the whole minimum. Note what is **not** in it:

- No `bic`, no `bank_account.institution` — derived from the IBAN.
- No `bank_account.account_holder` — defaults to the profile name.
- No `email` inside `profile` — the top-level one is the investor's address.
  Sending it in both places is a `422`.
- No `given_at` on any consent — the platform stamps them on receipt. If you
  captured the consent earlier, in your own UI, send the instant you recorded:
  `"terms": { "given": true, "given_at": "2026-08-14T09:12:04Z" }`. That is the
  better call for a regulated record, and the reason each consent is an object
  rather than a bare boolean.

`address`, `bank_account` and `company` are each **all-or-nothing**. Omit a
section entirely to leave it unset and the profile in `DRAFT`; send it and every
field it names is required, except inside `bank_account`, where only `iban` is.
`consents` is different — it is always required, and §7 of the
[investing flow](./investing-flow.md) explains why omitting it is destructive.

`partner_customer_ref` is **your** identifier for this person. Re-posting the
same ref with the same details replays the existing investor (`200`) instead of
creating a second one, so a retried call is safe.

## 5. Confirm the profile is complete

```bash
curl -sS "$VV_HOST/v1/investors/$VV_INVESTOR_ID" \
  -H "Authorization: Bearer $VV_TOKEN" | jq '{is_profile_complete, missing_profile_fields}'
```

```json
{
  "is_profile_complete": true,
  "missing_profile_fields": []
}
```

If it is `false`, `missing_profile_fields` names exactly what is still needed —
send those through `PUT /v1/investors/{investorId}/profile` and check again. An
order placed against an incomplete profile is refused with `409
INVESTOR_PROFILE_INCOMPLETE`.

## 6. Place the order

```bash
export VV_PURCHASE_ID=$(curl -sS -X POST \
  "$VV_HOST/v1/investors/$VV_INVESTOR_ID/share-purchases" \
  -H "Authorization: Bearer $VV_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d "{
    \"cooperative_id\": \"$VV_COOP_ID\",
    \"share_count\": 2,
    \"sepa_mandate\": { \"signed_at\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\" },
    \"statute_consent_given_at\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"
  }" | jq -r .share_purchase_id)

echo "$VV_PURCHASE_ID"
```

Four fields. **The cooperative, the share count, and the two moments you
captured that we cannot** — when the investor signed the SEPA mandate, and when
they consented to this cooperative's statute.

There is no price field, no total, no fee. Commercial terms are computed from
the cooperative's current pricing and frozen server-side at submission, so a
partner cannot dictate or drift what the investor pays. They come back on the
response.

Always send an `Idempotency-Key`. A retry with the same key and the same body
replays the original order instead of booking a second one.

## 7. Read the frozen terms

```bash
curl -sS "$VV_HOST/v1/investors/$VV_INVESTOR_ID/share-purchases/$VV_PURCHASE_ID" \
  -H "Authorization: Bearer $VV_TOKEN" | jq '{status, share_count, price_per_share, entry_fee, total, total_currency}'
```

```json
{
  "status": "submitted",
  "share_count": 2,
  "price_per_share": "100.00",
  "entry_fee": "0.00",
  "total": "200.00",
  "total_currency": "EUR"
}
```

This is the number to show the investor. It will not change.

Amounts are decimal **strings**, here and in webhook payloads alike. Parse
them into a decimal type — never a float.

## 8. Wait for settlement

The order is now `submitted`. Approval and settlement happen on our side, over
days. You find out one of two ways:

**Webhooks (recommended).** Ask partner support to register your endpoint and
you receive `share_purchase.approved`, then `share_purchase.settled` — or
`share_purchase.rejected` if it does not go through. Every payload carries
`terminal`, so you know when to stop waiting. See [Webhooks](./webhooks).

**Polling.** Re-read the order. Poll no faster than once a minute; nothing moves
in seconds.

```bash
curl -sS "$VV_HOST/v1/investors/$VV_INVESTOR_ID/share-purchases/$VV_PURCHASE_ID" \
  -H "Authorization: Bearer $VV_TOKEN" | jq -r .status
```

Once it is `settled`, the position is real:

```bash
curl -sS "$VV_HOST/v1/investors/$VV_INVESTOR_ID/holdings" \
  -H "Authorization: Bearer $VV_TOKEN" | jq '.items'
```

```json
[
  {
    "id": "9c1f7b02-4a55-4c6e-9f8b-2d3a1e5c7b90",
    "cooperative_id": "11111111-1111-4111-8111-111111111111",
    "investor_id": "b0d2f8e1-5c3a-4a77-9c11-2e6b8d4f0a35",
    "shares": 2,
    "pending_shares": 0,
    "first_acquired_at": "2026-08-14T11:02:07.884+02:00",
    "created_at": "2026-08-14T11:02:07.884+02:00",
    "updated_at": "2026-08-14T11:02:07.884+02:00"
  }
]
```

`shares` is the settled position. `pending_shares` is what is still on its way —
orders placed but not yet settled. The holding carries no cooperative name; join
on `cooperative_id` against the catalogue you already fetched.

## What to do next

- Read [The investing flow](./investing-flow) for the states, the gates, and
  what each call actually requires.
- Wire up [Webhooks](./webhooks) so you are not polling in production.
- Read [Errors](./error-reference) before you write your retry logic, not after.
