Exchange is excluded from this API/CLI release. Any historical Exchange API or CLI example in this guide is unavailable; native portal behavior is outside this exclusion. See [current release scope](/developers/guides/release-scope?lang=en).

# Explicitly linking accounting contacts

This workflow links one existing WeFact contact to one deliberately selected modern portal company. It uses the same service in the administration page, API and CLI. A matching name, email address, Chamber of Commerce or VAT number grants no ownership. The administration context (`--account`) is not the target company.

A current human internal administrator with recent passkey verification is required. The API requires `admin:read` for inventory and review, and `admin:write` for linking. Existing API, sensitive-action and administration flags remain effective. Service integrations cannot perform these human actions.

## Browser

Open **Administration → WeFact**. Search one bounded page of accounting contacts and select an exact contact ID. Enter the modern target company ID. Check the company name and current accounting contact details in the review window and explicitly confirm the target company. The review expires after ten minutes; changed identity, source configuration, company version or contact requires a new review.

Linking changes only the local accounting identity. It changes no bank account, direct debit mandate, login email or supplier data and sends no mail. Existing and archived ownership bindings remain protected. An unresolved original debtor creation requires investigation of that original record first.

## Importing as a new company: portal, API and CLI

The existing import section can import up to 25 selected contacts as **new** companies with new owners. Obtain a current import review for each selection. Check the proposed company name and exact `proposed.owner_email`; the browser must not derive that address from multiple accounting addresses. A missing or invalid single address makes import unavailable. Welcome mail is a separate opt-in field, disabled by default.

The server verifies every selected contact review before any creation. All new companies, owners and memberships are stored in one local transaction. A changed recipient or late local failure therefore cannot leave a partially created selection. An existing company or identity is never automatically adopted through email similarity. For existing companies, use the explicit linking workflow above.

Optional welcome mail starts only after local commit, through the existing separate identity-mail journal. The response binds each `remote_identifier` to the actual created `company_id` and `user_id`. If mail is uncertain, the company remains and the original mail attempt must be investigated; importing again does not resend that mail. Portal, API and CLI use the same import service. The receipt with the original `company_id`, `user_id` and `remote_identifier` is stored atomically with all new resources. An import has one caller-chosen UUID (`import_intent_id`); preserve it before sending. Repeating exactly the same body and UUID reads only the original evidence, even with a new HTTP idempotency key or after the contact review expires. A changed selection under the same UUID gives `409`. Only the original winning creation may start optional welcome mail; recovery reads only original mail metadata, even if the process stopped between local commit and mail.

## API and agents

All paths start with `/api/v1/accounts/{administratiecontext}`:

| Method | Path | Behavior |
|---|---|---|
| GET | `/admin/wefact/contacts?search=...&offset=0&limit=50` | One page, up to 100 contacts; continue with `data.next_offset` |
| GET | `/admin/wefact/contacts/{remote_identifier}` | One exact accounting contact |
| POST | `/admin/wefact/import-review` | One exact new company/owner review: `{remote_identifier}`; read-only |
| POST | `/admin/wefact/imports` | One atomic selection with a durable original receipt; original idempotency key |
| GET | `/admin/wefact/imports?limit=25` | Most recent own imports within the same administration context; continue with `meta.next_cursor` as `cursor` |
| GET | `/admin/wefact/imports/{import_intent_id}` | Retrieve original IDs and mail metadata, without supplier reads or mail |
| POST | `/admin/wefact/link-review` | Read-only; body `{company_id,remote_identifier}` |
| GET | `/admin/wefact/suggestions?limit=50` | Unverified local ID pairs; continue with `meta.next_cursor` |
| POST | `/admin/wefact/suggestions/{suggestion}/actions/reject` | Local rejection with current suggestion `If-Match` and idempotency key |
| POST | `/admin/wefact/link` | Reviewed local link; current `If-Match` and original `Idempotency-Key` |

The review response contains `company.id`, `company.name`, `company.version`, the safe `contact` projection, `reviewed_at`, `expires_at` and `review_token`. The projection contains no IBAN, mandate values, free-form supplier comments or API keys. Hidden billing settings and accounting source configuration do contribute to the opaque review version.

For linking, send only:

```json
{
  "company_id": 123,
  "remote_identifier": 456,
  "company_version": "<company.version uit de review>",
  "reviewed_at": 1793250000,
  "review_token": "<review_token uit dezelfde review>",
  "confirm_company_id": 123
}
```

`If-Match` contains the same `company.version`. `dry_run:true` performs current validation and source reads without a local link, audit mutation or operation. The actual local link, audit row and API operation outcome are stored atomically. Repeating the same key and body reads that original outcome without a new supplier read.

The import body contains only `import_intent_id`, `send_welcome` (default `false`) and a `contacts` list of 1–25 unique `{remote_identifier,reviewed_at,review_token}` values from import reviews. Review **all** proposed new companies and owner addresses before committing. `dry_run:true` performs the same current validation without creation, receipt, operation or mail. Import creates only local companies, owners and memberships; there are no accounting mutations, invoices or payments.

After a lost response, an agent requests **GET imports/{original UUID}**. The browser also shows that reference before sending and has a recovery screen plus the 25 most recent own receipts. A `404` does not prove that a concurrent original request will never commit: check the same reference later and do not start a new import. Recovery requires current human administration rights, the same actor and the same administration context; another actor or context cannot adopt the original identity. New contact reviews are needed only for a new, separately approved import. Do not store review tokens or recipient data in browser storage.

## CLI

```sh
binadit admin wefact contacts --account 1 --limit 50
binadit admin wefact contact 456 --account 1
binadit admin wefact suggestions --account 1
binadit admin wefact reject-suggestion 789 --version "<suggestie-versie>" --confirm-suggestion 789 --account 1
umask 077
binadit admin wefact review-link 123 456 --account 1 --output json > reviewed-contact.json
binadit admin wefact link --from-file reviewed-contact.json --confirm-company 123 --account 1 --dry-run
binadit admin wefact link --from-file reviewed-contact.json --confirm-company 123 --account 1
# Ook meerdere contact-ID’s (maximaal 25) kunnen samen worden beoordeeld.
binadit admin wefact review-import 456 457 --account 1 --output json > reviewed-import.json
# Lees eerst de exacte bedrijfsnamen/eigenaaradressen en de import_intent_id in dit bestand.
binadit admin wefact import --from-file reviewed-import.json --confirm-import "<oorspronkelijke UUID>" --account 1 --dry-run
binadit admin wefact import --from-file reviewed-import.json --confirm-import "<oorspronkelijke UUID>" --account 1
binadit admin wefact import-receipt "<oorspronkelijke UUID>" --account 1
binadit admin wefact imports --account 1
```

Use `review-import --send-welcome` only after explicit permission for the separate welcome-mail attempts. The review file is bound to server and administration context. The CLI confirms only the exact same contact selection and projects only the actual created IDs; unexpected supplier or SMTP fields are not printed.

For a next contact page, explicitly use `--offset` from the previous response. `--all` is unavailable here. The protected review file may also be supplied through stdin with `--from-file -`. Check displayed identities before confirming the link; an agent must not infer the target company from an email match.

On `412`, obtain a new review. On `409`, investigate the existing ownership binding or original debtor attempt. After an uncertain API response, inspect the original operation and reuse the same intent; after an uncertain browser response, first inspect the selected company. Do not create a replacement debtor or payment to resolve an uncertain local link.

This API/CLI use case covers inventory, explicit existing-contact linking, current local suggestion rejection and reviewed new-company import with an original receipt. Debtor creation/push, direct debit management and general payment administration have further workflows; this guide does not automatically declare those features fully available.

Source contracts: [WeFact contact list](https://developer.wefact.com/debtor/list) and [exact contact detail](https://developer.wefact.com/debtor/show). The existing canonical transport layer retains its limits, TLS/redirect policy and current authorization checks.
