# ElvixLegalEntityPicker

> Pick which company an invoice or a payout is for, or add one inline. Your backend then reads it from elvix by id, with elvix's own VAT and name checks.

A list of the user's legal entities: one tap picks, and the picked row stays tinted. 'Add a company' runs the add wizard inline (VAT ID first, prefilled from the register) and comes back with the new one picked. There is no confirm button and no done pane: your own Continue ends the step.

<!-- app/bookings/invoice-company.tsx -->
```tsx
"use client";

import {
  ElvixLegalEntityPicker,
  type LegalEntityRecord,
  useElvixLegalEntityGrant,
} from "@elvix.is/sdk/react";
import { useState } from "react";

export function InvoiceCompanyStep({ onContinue }: { onContinue: (entityId: string) => void }) {
  const [picked, setPicked] = useState<{ entity: LegalEntityRecord; granted: boolean } | null>(null);
  const grant = useElvixLegalEntityGrant();
  return (
    <div className="invoice-pane">
      <ElvixLegalEntityPicker
        card={false}
        types={["sole_prop", "company"]}
        selectedId={picked?.entity.id ?? null}
        onPick={(entity, { granted }) => setPicked({ entity, granted })}
      />
      {/* Your own Continue. A pre-selected default is not granted yet: grant it here. */}
      <button
        type="button"
        disabled={!picked}
        onClick={async () => {
          if (!picked) return;
          if (!picked.granted && !(await grant(picked.entity.id)).ok) return;
          onContinue(picked.entity.id); // send ONLY the id to your backend
        }}
      >
        Continue
      </button>
    </div>
  );
}
```

## Props

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `onPick` | `(entity: LegalEntityRecord, pick: { granted: boolean }) => void` | required | — | A tap or a save in "Add a company" grants first, then calls `onPick(entity, { granted: true })`. The pre-selection (the default, else the only entity) calls `onPick(entity, { granted: false })` with no request: grant it from your Continue with `useElvixLegalEntityGrant()`, so nothing is shared without the user's step. |
| `selectedId` | `string \| null` | optional | — | The picked entity, controlled by you. Pass the current id to reopen with it picked ("change company"). |
| `types` | `("individual" \| "sole_prop" \| "company")[]` | optional | all three | Which entities are listed and which "Add" offers. One type skips the type question. |
| `purpose` | `"invoicing" \| "payouts"` | optional | `"invoicing"` | What the pick lets your backend read, and how "Add" asks. Invoicing: the VAT ID (or tax number), the legal name prefilled from the register, the address, an optional invoice email; nothing else, except an e-invoice address for Italy (SDI code or PEC) and Belgium (Peppol ID), the two places a B2B invoice cannot be sent without one. Payouts: the full form, with birth data and registration skippable, read as a prefill for your payment provider; the picker says so above the list, and elvix mails the person each new payouts grant with a link to stop it. The user sees which on elvix.is and can stop each. |
| `card` | `boolean` | optional | `true` | `false` drops the elvix card so the picker sits in your own pane. |
| `sizing` | `"fixed" \| "flexible"` | optional | `card ? "fixed" : "flexible"` | Fixed: the list and every add step share one height (`min(520px, 70dvh)`, or `height`) and scroll inside it. Flexible: as tall as the step showing, no inner scroll, your page scrolls. |
| `height`, `minHeight`, `maxHeight` | `number \| string` | optional | — | Fixed mode's height; `maxHeight` caps either mode (and then scrolls inside). |
| `onResult` | `(result) => void` | optional | — | `{ ok: true, entityId }` after a grant, `{ ok: false, error }` when it or a save fails (`grant_failed`, `missing_client_id`, `save_failed`). The failure is also shown in the frame with a retry. |

## What onPick hands you

The entity as your page may see it. Render the step from it; never post it to your backend as the invoice's data, because a browser can change anything it holds.

| Field | Notes |
| --- | --- |
| `id` | What you send to your backend. Nothing else from the browser. |
| `withheld` | `true` in your app: birth date, place of birth, nationality, phone (and an individual's tax number, home address and the register's copy of either) come back `null` here and are write-only. Your backend reads them with a `payouts` grant. |
| `vatIdValidation` | elvix's own check, never the browser's: `live` (the register confirmed the number), `format`, `none`. |
| `vatIdNameMatch` | `live` proves the NUMBER exists, not the company: `match`, `mismatch` (another company's VAT ID), `unknown` (the register shares no name, e.g. Germany). |
| `vatId` | Normalised as printed on an invoice: uppercase, no separators, with its country prefix. |
| `givenName`, `familyName` | A person's name in its two parts (individuals and sole proprietors). |
| `peppolId`, `codiceDestinatario`, `pecAddress`, `leitwegId` | Where e-invoices go, when the business gave one: Peppol (`<scheme>:<id>`), Italy's SDI code or PEC, Germany's Leitweg-ID (public bodies). The add flow asks only Italian (SDI code or PEC) and Belgian (Peppol ID) businesses; elsewhere an e-invoice goes by email or is routed by the VAT ID itself (DE, PL, FR, RO, HR). Your backend also gets `vatIdScheme`, `taxIdScheme` and `registrationNumberScheme`: the Peppol EAS codes to label each number with (`9930` = German VAT ID). |
| `address*` | Line, postal code, city, region, ISO country: required for businesses (VAT Directive art. 226(5)). |

## Your backend reads it from elvix

Every grant is per app and per purpose, until the user stops sharing it at elvix.is (their account's app page or Tax & business). A read-only API key is enough; the grant is the gate. Also an MCP tool (`get_applications_id_users_uid_legal_entities_entityId`) and in the API reference.

<!-- server/invoice.ts -->
```ts
import { getElvixLegalEntity } from "@elvix.is/sdk/server";

// The browser sent only the id. Read the rest from elvix.
const result = await getElvixLegalEntity({
  apiKey: process.env.ELVIX_API_KEY!,   // a READ-ONLY key is enough
  applicationId: process.env.ELVIX_APP_ID!, // the internal id, not the clientId
  userId: session.sub,                  // the signed-in user's elvix id
  entityId,
  purpose: "invoicing",                 // or "payouts", whichever the user granted
});
if (!result.ok) {
  // "not_found": not granted for this purpose, revoked, deleted, another user's,
  // or the user left your app. "invalid_id": not an id (no request was made).
  throw new Error(`company not available: ${result.error}`);
}
const company = result.entity;
if (company.vatIdNameMatch === "mismatch") {
  // A real VAT ID under another company's name: do not issue a reverse-charge invoice.
}
```

```bash
curl "https://elvix.is/api/applications/$APP_ID/users/$USER_ID/legal-entities/$ENTITY_ID?purpose=invoicing" \
  -H "Authorization: Bearer $ELVIX_API_KEY"

# 200 { "success": true, "data": { "entity": { "purpose": "invoicing", "grantedAt", "updatedAt",
#   "id", "type", "legalName", "givenName", "familyName", "taxCountry", "taxId", "vatId",
#   "vatIdValidation", "vatIdValidatedAt", "vatIdValidatedName", "vatIdValidatedAddress",
#   "vatIdRequestId", "vatIdNameMatch", "vatIdScheme", "taxIdScheme", "registrationNumberScheme",
#   "registrationNumber", "registrationBody", "contactEmail",
#   "peppolId", "codiceDestinatario", "pecAddress", "leitwegId",
#   "addressLine1", "addressLine2", "addressPostalCode", "addressCity",
#   "addressRegionName", "addressRegionCode", "addressCountry", "addressCountryName", "addressFormatted" } } }
# ?purpose=payouts adds what the person saved of tradingName, dateOfBirth, placeOfBirth,
#   nationality, contactPhone, registeredSince (and an individual's taxId): a prefill
#   for your payment provider's own form (elvix collects no KYC)
# 404 not_found: one answer for every refusal, so ids cannot be probed
```

> **Note:** Set your own EU VAT ID on the app (Console → Legal, or `PATCH /api/applications/{id}` with `vatRequesterId`; saved only once VIES confirms it: `vat_requester_invalid`, or `vat_requester_unconfirmed` to retry while VIES is down) and elvix asks VIES in your name on reads, at most daily per grant: `vatIdRequestId` is then YOUR consultation number, the proof you checked the buyer's VAT ID, and `vatIdNameMatch` comes from VIES where the member state compares names. A consultation counts only for the VAT ID, name, address and requester it checked: after any change it is asked again, and until VIES answers the read carries none. Snapshot what you invoice from the read you invoice with.

> **Note:** Webhooks, only to apps holding a grant: `user.legal_entity_updated`, `user.legal_entity_deleted`, `user.legal_entity_revoked`, each with `{ userId, entityId, purpose, updatedAt }`. Snapshot the entity onto what you issue; an update later does not change an invoice already issued.

## Theming

Every colour, fill, radius and the focus ring is a CSS variable, set on any ancestor. Fields have a tint fill and no outline unless you set `--elvix-field-border`. The SDK sets no font, so yours applies.

<!-- app/globals.css -->
```css
/* Any ancestor of the picker. Map elvix's variables onto your own tokens,
   and light / dark follows your theme by itself. */
.invoice-pane {
  --elvix-field: var(--fill);            /* text field fill */
  --elvix-field-border: transparent;     /* no outline at rest (the default) */
  --elvix-field-focus: var(--fill-focus); /* focused: a deeper tint, never an outline */
  --elvix-field-radius: 12px;
  --elvix-row: var(--fill);              /* a list row */
  --elvix-row-hover: var(--fill-strong);
  --elvix-picked: color-mix(in srgb, var(--accent) 10%, transparent);
  --elvix-picked-text: var(--accent);    /* the picked row's name */
  --elvix-radius: 12px;                  /* rows and cards */
  --elvix-fg-1: var(--ink);              /* text */
  --elvix-fg-3: var(--muted);            /* secondary text */
  --elvix-focus: var(--accent);          /* focus ring */
  --elvix-danger: var(--error);          /* a field's error outline */
  --elvix-icon: var(--accent);           /* the icons in their round bubbles */
  --elvix-icon-bg: var(--fill-strong);   /* the bubbles behind them */
  --elvix-key-hints: inline-flex;        /* show the "Enter" chip (off by default) */
  font-family: inherit;                  /* the SDK sets no font */
}
```

> **Warning:** The picker needs `<ElvixProvider clientId>`: the grant is per app. Without one it says so in the frame and reports `missing_client_id`, rather than letting a pick your backend cannot read look done.


## Related

- [ElvixLegalEntities](/docs/components/elvix-legal-entities.md): Single-frame multi-step wizard for legal entities (individual, sole prop, company) with VAT live-validation and tap-to-edit.
- [ElvixAddressBook](/docs/components/elvix-address-book.md): Single-frame wizard for billing or shipping addresses with Google Places search, default toggle, and tap-to-edit.
