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).

# Registrar contacts: portal, API, CLI and original-request recovery

## Implementation boundary

The portal's contact create, edit and delete forms and the API use the same `HolderChanges` workflow and the portal's existing registrar connection. Public API availability still depends on the environment's API, write and sensitive-access settings. Installing this implementation does not activate a production release or publish CLI downloads.

## Using contacts

### Default contact from the existing account

`POST holders/default` with an empty JSON object previews or ensures the selected account's default registrar contact. It requires both `holders:read` and `holders:write`. It uses the account's existing legal/contact details; missing fields can be filled only from its uniquely linked WeFact debtor. It does not search by a similar name or email, create another debtor, or change billing or bank details.

```sh
binadit api /holders/default -X POST --account 123 --dry-run
binadit api /holders/default -X POST --account 123 --yes --no-input --output json
```

The preview returns `status` and, when a profile is incomplete, `missing_fields`. `pending_incomplete` does not create a registrar contact. Businesses retain their company details; consumers get a personal contact. Missing addresses, names, telephone numbers or gender are never invented. An already confirmed default is retained. Reusing an existing matching owned contact first verifies that exact handle with a registrar GET; it does not overwrite that contact.

New accounts follow this same workflow after their owner's email is verified and the required profile is complete. Public signup does not create registrar contacts for unverified identities. Setup completion, profile completion and an explicitly applied WeFact update can finish a pending default. Supplier work starts after the actual outer account transaction commits; a rolled-back signup/import sends no contact request. In domain registration, an owned default is suggested for a basket tied to the current account. An explicitly chosen contact is preserved; an old unbound basket or another company's contact is not guessed.

If creation has an uncertain outcome, retain its original mutation and use the recovery workflow below. Another key, profile update or automatic onboarding attempt cannot bypass the original hold. A contact used as an account default cannot be deleted while that reference remains.

### Explicit additional contacts

Use the selected modern company, with a uniquely linked registrar customer. `holders:read` permits a bounded contact list/detail and safe original-request metadata; `holders:write` permits ordinary changes under the company's current module policy. Restricted DNS/Workspace credentials cannot administer contacts. Ownership, handles, reseller links and supplier credentials are server-assigned.

`GET /api/v1/accounts/{account}/holders/{id}` returns the editable public contact fields, its `meta.etag` and HTTP ETag. Use that exact current ETag in `If-Match` for `PUT` or `DELETE`. A contact must be unused in every domain contact role before deletion. An unlinked local contact can be deleted without constructing a supplier connection; an exact documented supplier absence can also finish locally without pretending a DELETE happened.

Create with `POST holders`; replace the full editable contact with `PUT holders/{id}`. Relative paths in this guide are beneath `/api/v1/accounts/{account}/`. Both use this JSON contract:

```json
{
  "companyname": "Example BV",
  "initials": "A",
  "firstname": "Alice",
  "prefix": null,
  "lastname": "Example",
  "gender": "F",
  "street": "Example street",
  "number": "7",
  "suffix": null,
  "zipcode": "1234AB",
  "city": "Utrecht",
  "state": null,
  "country": "NL",
  "email": "alice@example.test",
  "tel1": "31",
  "tel2": "30",
  "tel3": "1234567",
  "fax1": null,
  "fax2": null,
  "fax3": null,
  "vat": null,
  "coc": "12345678"
}
```

`companyname` is the write-field spelling; the existing read projection calls it `companyName`. Required fields are initials, firstname, lastname, gender (`M`/`F`), street, number, zipcode, city, uppercase two-letter country, email, phone country code (`tel1`) and subscriber number (`tel3`). An omitted phone area (`tel2`) is sent as `0`; phone country codes gain the canonical `+` prefix. The OpenAPI schema gives exact lengths. Optional fields accept null; full replacement explicitly sends supported empty optional values and proves them in the supplier readback before clearing the local fields. Read-only IDs, handles and timestamps must not be copied into a replacement request. Gender is local portal metadata because the registrar's REST customer contract does not attest it. Private identification documents and birth details are outside this public edit contract.

Personal names and company names are immutable at the supplier. The portal marks these fields read-only when editing; API changes return `422 holder_name_is_immutable`. To change them, explicitly create and review a separate contact, then change each domain's contact roles through the domain workflow. Changing a contact does not itself change any domain role or DNS record.

The Rust CLI uses the same contact paths:

```sh
binadit holders get 7 --account 123 --output json
binadit holders create --account 123 --from-file contact.json --dry-run
binadit holders create --account 123 --from-file contact.json --yes
binadit holders update 7 --account 123 --version CURRENT_CONTACT_ETAG --from-file contact.json --yes
binadit holders delete 7 --account 123 --version CURRENT_CONTACT_ETAG --confirm-resource 7 --yes
```

Keep contact input in a protected file or trusted stdin (`--from-file -`). Keep the original body, company, principal and idempotency key after a timeout. The CLI's permanent principal binding also applies to these generic contact commands; a new login must never silently replay an uncertain original mutation. CLI source/build availability and signed public distribution are documented separately in the [CLI guide](/developers/cli?lang=en).

## Importing an existing registrar contact

Internal administrators can import an independently verified existing handle with `POST admin/holders/import` and a fresh personal `admin:write` grant/passkey. Send an original `Idempotency-Key` and `{"target_account_id":123,"handle":"AA123456-NL"}`. The target is the explicit modern company, whose legacy customer binding must be unique and active. A service credential or ordinary customer membership does not grant this staff capability. Existing local handles require explicit assignment; import never silently moves them.

Import sends only a bounded GET through the portal's configured registrar connection. It checks the current original identity generation, scope, staff role, strong verification, administration context, supplier source and target before and after that GET and again while publishing locally. Revocation or a changed target/source prevents publication. Local contact fields and the original API success receipt commit together; a cancelled or altered save fails closed. Replay of the exact original key returns its stored receipt without a second supplier GET. A final committed-state and current-authority check also runs before reporting success. If authority/source changes immediately after the local commit, the contact may already exist even though the current request reports an error; inspect the original receipt and portal state before another import.

The browser's **Add with handle** action binds the company context rendered when the page opened. Reload after switching companies. Import accepts the registrar's REST `snake_case` contact fields and the established legacy aliases; conflicting values are refused. Phone/fax components, registration numbers and birth details retain their original mapping. Identification fields and supplier comments remain outside public API/CLI responses. The portfolio synchronizer uses the same pending-create hold, so import/sync cannot bypass an unresolved original contact create.

## An uncertain result

An interrupted supplier mutation returns `409 holder_mutation_requires_reconciliation`, with safe `errors.intent_id` and `provider_mutation_repeated: false`. The original operation result also carries `holder_mutation_id`. This UUID identifies the original business request; it must not be replaced with another UUID/key. A failed HTTP operation can follow an accepted supplier effect.

The browser shows **Review original request** and opens `/module_holders/mutations/{originalUUID}`. It preserves the form UUID/version and does not automatically resubmit. The review page shows safe state only and offers explicit verification. A current company owner must first verify using a passkey or authenticator under the current company policy; internal administrators require their current passkey. Ordinary module writers and service integrations cannot recover another original contact request. Current owner recovery can finish an original portal or API request in that same company without changing its historical principal or supplier intent.

API recovery uses these endpoints:

| Method and path | Purpose |
| --- | --- |
| `GET holders/mutations/{originalUUID}` | SQL-only safe state, current journal ETag and original holder ID; no supplier request |
| `POST holders/mutations/{originalUUID}/actions/reconcile` | Supplier GET only, then atomic local completion of the original request |
| `POST holders/mutations/{originalUUID}/actions/abandon` | Cancel only a signed preparation proven never dispatched |

Reconcile and abandon require `holders:write`, enabled sensitive actions, a current unrestricted personal owner connection with recent strong verification, a new HTTP idempotency key for this separately approved action and the journal ETag in `If-Match`. Supply:

```json
{
  "confirm_intent_id": "11111111-1111-4111-8111-111111111111",
  "confirm_holder_id": 7,
  "confirm_holder_version": "CURRENT_64_HEX_CONTACT_ETAG",
  "confirm_no_supplier_mutation": true
}
```

For an original create, `confirm_holder_id` is null and there is no contact version. For update/delete read the current original contact first and confirm its exact unquoted 64-hex version. The journal version and contact version are separate proofs. Once an original deletion is complete its missing local contact does not prevent receipt lookup. `412` requires a fresh read and review; it is not permission to repeat the supplier effect.

If a create acknowledgement was lost, add `verified_provider_handle` only after an operator independently establishes the exact original handle from the supplier. Reconciliation GETs that explicit handle and requires its exact private original-request comment and every approved contact value from the same configured supplier. Identical name/email, a missing comment or unknown handle remains held. The private correlation MAC is not a public capability and is never shown in API, CLI or audit output. A changed supplier identity, ambiguous ownership or unverified response requires operator investigation; do not edit the journal, reset the dispatch marker or import a candidate to bypass it. A pending unknown create conservatively blocks registrar contact import until it is investigated; acknowledged creates block importing their exact original handle.

The generic Rust API command exposes the same explicit recovery contract without another supplier mutation:

```sh
binadit api /holders/mutations/ORIGINAL_UUID --account 123
binadit api /holders/mutations/ORIGINAL_UUID/actions/reconcile --method POST \
  --account 123 --version CURRENT_JOURNAL_ETAG --from-file recovery.json --yes
```

After success, read the original request again and check `completion_verified: true` and `requires_reconciliation: false`. `provider_acknowledged` remains false when recovery proved a lost acknowledgement through GET; recovery never invents an original ACK. Never retry a POST/PUT/DELETE as a verification probe.

## Backend evidence and security boundary

The shared `HolderMutations` service owns short SQL transactions for `prepare`, `inspect`, `begin`, `acknowledge`, `uncertain`, `confirm`, `complete` and `abandonPrepared`. Every public phase requires transaction level zero at entry. Supplier I/O happens outside these transactions. The caller's original authority adapter performs current policy reads under the phase's locks and returns a pure final clock fence; that fence runs again after commit before the caller can dispatch a request.

The private journal binds the original operation/business UUID, original API request/key fingerprint envelope, principal, actor/security generation, modern company, uniquely mapped legacy owner, supplier source, action, target contact/handle and approved version. It encrypts bounded original input, authority context, supplier acknowledgement and readback. Keyed proofs bind those fields, the committed single dispatch witness, readback and terminal local receipt. Private evidence, supplier credentials and internal correlation comments never belong in public operation metadata or logs. Normal authorized contact responses contain the explicit public contact projection.

One active target hold prevents another key from changing the same contact. An uncertain create also holds the same company and provider payload; another UUID cannot evade it. A dispatched intent can never return to a new dispatch by resetting status or its timestamp: the independent signed witness must still agree. Only a signed preparation that was never dispatched can be abandoned without supplier evidence. Completed history releases a hold only after signed terminal verification, without decrypting every old contact payload.

`begin` commits before the actual customer mutation. Every journal save requires a true result and exact freshly persisted intended fields; an event-cancelled save cannot advance a phase. The actual wire guard then checks the committed signed marker and current original authority through autocommit reads with a final pure clock, without opening another verification transaction. Cancelled local saves/deletes and cancelled original API receipt updates also retain the original hold instead of reporting completion. `acknowledge` retains a validated original acknowledgement even after authority is lost. Neither method permits a second mutation. `confirm` accepts only a trusted, pure backend verifier of the original exact readback. `complete` publishes the local contact and original parent receipt atomically with the signed completion state, after current authority, ownership, version and source checks. These proofs do not make arbitrary whole-database rollback or server compromise safe.

## Canonical supplier contract

The strict clone keeps the existing configured registrar and cURL transport. It requires HTTPS outside a private loopback test. Its token cache is keyed by the full private credential-source proof, so changed credentials cannot reuse a previous strict session. The base SDK's legacy behavior and cache namespace remain unchanged.

`withDispatchGuard` remains a current-authority fence for authentication and all supplier requests. `withCustomerMutationGuard` marks only the actual customer POST/PUT/DELETE after authentication. Authentication and GETs cannot consume that journal marker. Strict customer mutations never automatically authenticate and repeat after an HTTP 401.

The strict response contract requires a successful HTTP response, integer supplier code `0`, and a typed data envelope. Create requires the canonical contact handle. Update/delete require `data.success === true`; an empty body or truthy string is insufficient. GET must identify the exact handle, a positive supplier reseller ID and a boolean deleted state. An HTTP 404 alone does not prove absence. The documented supplier code `176` identifies a missing customer handle; the recovery adapter binds it to the exact original GET and source.

The update operation does not change a contact's name or company. It submits only the supported mutable fields; readback verifies both the unchanged name/company fields and all approved mutable contact fields. The shared workflow rejects changes to immutable fields and publishes only values confirmed by the registrar. A contact can be deleted only when no domain or other service resource still references it.

## Recovery rules for users and AI agents

Preserve the original company, intent/operation UUID, contact ID/handle, approved version and original key. A failed or interrupted request can follow an accepted provider effect. Do not allocate another UUID or POST as a recovery probe. Reconciliation may read the exact original handle and finish a proven local publication; it must not send another create, update or delete.

A changed original API request envelope also remains held: a new owner recovery cannot replace its method/body/key identity or bless altered parent metadata. An uncertain create without a returned handle remains held. Matching another contact by name or email is not proof that the original create produced it. Recovery needs a verified original supplier receipt or operator investigation. Do not infer absence or no charge from an unknown result.

The local tests cover encrypted evidence, duplicate holds, changed source/owner/version, forged terminal state, dispatch-marker reset, late acknowledgements and rollback of local publication. A disposable socket-only MariaDB trial proves committed visibility, current ownership after an older repeatable-read snapshot, and a real hard-killed canonical HTTP request that cannot cause a second POST. The private HTTP fixture verifies documented response shapes, credential-rotation authentication, one customer marker after login, and refusal to repeat after HTTP 401. These tests contact no customer or external provider.


## Browser context and the original pending marker

Native create, edit and delete requests must include the originally rendered
`holder_context_account_id`. The portal supplies this hidden value. A second tab
switching the active company cannot redirect an older form's contact creation;
the server refuses it before a registrar request. Return to the original company
and reload rather than replacing an uncertain original UUID. Existing recorded
requests remain held and keep their original review link.

For staff editing a foreign company's contact, the actual contact owner and the
original administration context are separate. The recovery page keeps both. It
clears only a terminal, matching UUID marker under the original browser context,
not a similarly numbered target-company marker. An optional
`browser_context_account_id` on the review link selects only the current
authorized browser context or the original recorded context. It never grants
access, selects a business owner or changes the registrar target.

## Platform administrators: contacts in another company

Platform administration uses `/api/v1/accounts/CONTEXT_ID/admin/holders`.
`CONTEXT_ID` is the current personal connection's administration context, not an
implicit contact owner. A current internal administrator, recent passkey and the
appropriate `admin:read` or `admin:write` scope are required. No membership is
created in the target company. Machine connections cannot administer contacts.

| Action | API | Rust CLI |
| --- | --- | --- |
| Bounded public list | `GET admin/holders?limit=25&cursor=0&search=AA` | `admin holder-contacts list --limit 25 --search AA` |
| Inspect domain contact roles | `GET admin/holders/CONTACT_ID/domains` | `admin holder-contacts domains CONTACT_ID --limit 25` |
| Read editable fields and ETag | `GET admin/holders/CONTACT_ID` | `admin holder-contacts get CONTACT_ID` |
| Create for an explicit company | `POST admin/holders` | `admin holder-contacts create --confirm-company COMPANY_ID --input contact.json --yes` |
| Replace mutable fields | `PUT admin/holders/CONTACT_ID` | `admin holder-contacts update CONTACT_ID --version ETAG --input contact.json --yes` |
| Delete an unused contact | `DELETE admin/holders/CONTACT_ID` | `admin holder-contacts delete CONTACT_ID --confirm-holder CONTACT_ID --version ETAG --yes` |
| Inspect an original request | `GET admin/holders/mutations/ORIGINAL_UUID` | `admin holder-contacts mutation ORIGINAL_UUID` |
| Read-only original recovery | `POST admin/holders/mutations/ORIGINAL_UUID/actions/reconcile` | `admin holder-contacts reconcile ORIGINAL_UUID --confirm-mutation ORIGINAL_UUID --input recovery.json --yes` |
| Abandon a never-dispatched preparation | `POST admin/holders/mutations/ORIGINAL_UUID/actions/abandon` | `admin holder-contacts abandon ORIGINAL_UUID --confirm-mutation ORIGINAL_UUID --input recovery.json --yes` |

Append `--account CONTEXT_ID` to each CLI command. List pagination is explicit:
use the returned `meta.next_cursor` with `--cursor`; at most 100 contacts are
returned. Search treats `%` and `_` literally. Authorized contact output contains
the editable public fields, never private identity documents, original provider
evidence, credentials or internal correlation comments.

`contact.json` uses the customer contact fields described above. Create also
requires matching integer `target_account_id` and `confirm_target_account_id`.
The CLI `--confirm-company` must match both. Update cannot change a name or
company name, move ownership, replace the provider handle or change reseller
identity. Explicit local assignment/import remains a separate administration
operation. Delete JSON contains `confirm_holder_id`. New writes use the normal
durable idempotency key and `--dry-run` review; a dry run sends no supplier
request and creates no contact journal.

To recover, first read the original metadata and retain its ETag. This is the
journal ETag, distinct from the current contact ETag. For example:

```json
{
  "version": "CURRENT_JOURNAL_ETAG_64_HEX",
  "confirm_intent_id": "ORIGINAL_UUID",
  "confirm_holder_id": 123,
  "confirm_holder_version": "CURRENT_CONTACT_ETAG_64_HEX",
  "confirm_no_supplier_mutation": true
}
```

Use `confirm_holder_id: null` for an original create. An uncertain create without
a retained handle also needs `verified_provider_handle` from verified original
supplier evidence; never infer ownership from a matching name/email. The Rust
CLI removes `version` from this JSON and sends it as `If-Match`. Direct API
clients send that header themselves and omit the extra `version` property.
Recovery requires **both** current admin scopes and a recent passkey, but new
supplier writes may remain disabled. Do not send `Idempotency-Key`, `--dry-run`,
`--wait` or another UUID to these recovery actions. They allocate no new API
operation. Abandon refuses every dispatched or uncertain original.

A current authorized administrator may explicitly recover a customer API,
native portal or staff API original through this adapter. The signed original
target company, actor, principal, source, request envelope and parent operation
remain unchanged. Staff API parents belong to their original administration
context; the contact journal and contact belong to the actual target company.
This cross-context distinction is checked against the original private receipt
before supplier reads and local publication. A new connection does not grant
permission to automatically resend an old write.

For AI agents: use `HolderAdministratorChanges` solely as an authority/transport
adapter around `HolderChanges`, `HolderMutations` and `HolderPublication`.
Preserve their single-dispatch witness and current source/ownership checks. A
staff parent uses business/route action `admin.holders.create`, `.update` or
`.delete` and its existing canonical private operation receipt; do not rewrite
it to the target company's customer namespace. Policy reads use the canonical
primary, and a final pure clock follows the last read without a new commit.

These commands are implemented in source and covered by local contract tests.
Public binary publication and production activation remain governed by the
overall API/CLI release checks.
