# Rapportly API: from synthetic source to result

The existing API baseline was tested and released to production on 4 October 2026. The CRM writes with 375 have been released to production after a controlled rollout. The synthetic CRM test verified saved searches, visit markers and trajectories, including replay, revision conflicts, permission rejection, removal of its own test client and revocation of the temporary keys. Office templates were only read in this production test; their write contract was tested locally and in CI. Workflow language processing with 376-v2 and comparison of saved output history were tested locally and in CI. Their full production behaviour is not established by this evidence; the CRM test does not verify those features. OpenAPI identifies the exact contract source per route; the historical route inventory is 56179ec5. Use only actions released for the specific environment. This does not mean complete parity with the website. Paid continuation of individual language outcomes from 28d23f2e and bulk processing with a second language remain excluded. CRM source d964db8c and workflow language source a2f438b8 still require current permissions, privacy approval and complete schema readiness.

## Release evidence and scope

- Backend merge: `d1369da2f17f32fa2bffbcc361951e379a62e218`; deployment: `c9ab886d-c92d-48b7-a182-a3e5f7ead4d4`.
- Public runtime/source readback: `1016c3ab4030f19b719a0423e7db03ca2aa80e42` / `72c1e27dfc3eb3c90689840a8f8e7a8b6ab0688f` at `2026-10-05T14:41:33.902190+00:00`.
- CRM test: `https://github.com/ASKmimon/rapportly-app/actions/runs/37325544706`; content-free evidence: `fb6f357e0fd04d5ed94bf95a076d36ae9624eabc0aba1df93305a8f2a30bedb2`; completed at `2026-10-05T14:33:53Z`.
- Migrations 375 and 376-v2, exact privacy approval, readiness and CRM activation: `375: 20261005121746; 376-v2: 20261005130842; 12 readiness / RLS / ACL checks passed; PostgreSQL 17 CI 37320873679; CRM concurrency CI 37320873720`.

The CRM proof uses temporary keys with a zero credit budget and verifies cleanup and revocation. It covers three CRM writes; office template changes were not made in production. Workflow language processing and saved-history comparison have local/CI evidence only in this release report. Bulk processing with a second language remains closed. This is neither full website parity nor external customer acceptance.

## Choose the contract and import the examples

`rapportly-productie.openapi.json` describes real processing routes, with explicit schemas and an open list on partial operations. `rapportly-schema-inventaris.json` inventories the remaining static actions whose exact contracts still need work. `rapportly-productie.postman_collection.json` is a Postman Collection v2.1 with manual requests. The separate test-mode specification and collection describe a simulator with fixed fictional output.

The production collection defaults to `https://api.rapportly.invalid/api/rapportly`, empty credentials and IDs, and `allowMutations=false`. Importing creates no key, file or job. Its preparation script blocks the invalid address and all POST/PUT/PATCH/DELETE requests until deliberately enabled locally. This also blocks the estimate POST, although that route starts no processing and spends no credits.

Use a separately released organisation with synthetic information only. Deliberately set `baseUrl` to the confirmed HTTPS server ending in `/api/rapportly`, for example the intended address `https://api.rapportly.io/api/rapportly`. Requests add `/v1`. Keep `apiKey` and, when needed, `sessionJwt` local and secret. Do not export filled credentials. Enable `allowMutations` only for a reviewed request and disable it afterwards. Do not run the collection as an unattended sequence.

No collection import or API execution has been performed for this checkpoint. The following are examples for a later released trial. Real API actions using fictional text can still consume credits, storage and provider services.

## Keys, rights and budget

A production key starts with `rply_`; a simulator key starts with `rply_test_`. Send `Authorization: Bearer <key>`. A test key only works with the simulator. The associated user, organisation, package, object access and administrator rights ceiling remain authoritative.

Read `GET /v1/acties` with `gebruik:lezen` for connected actions. `toegestaan_door_sleutel` does not grant access to every object. `GET /v1/gebruik` returns this key's monthly budget and request counts. Open reservations from earlier months still count; actual consumption is assigned to the UTC reservation month. An unlimited key budget does not add account credits.

Key-management routes under `/koppelingen/sleutels/{id}` are outside `/v1` and require a user session JWT. An API key cannot replace that session. Read the current ETag before changing policy and send it unchanged, including quotes, as `If-Match`. Team members can only tighten policy within granted rights and the administrator ceiling. A key never switches between test and production. Replacing its secret takes effect immediately; after a lost response, read current state before taking further action.

## Journey A: source file, report, status and download

1. Generate and save two UUIDs locally as `dossierId` and `bestandId`. Send `PUT /v1/dossiers/{dossierId}` with `{"naam":"Synthetic Atlas workshop"}`. Creation returns 201; an identical replay by the same owner returns 200. This PUT uses its UUID and needs no Idempotency-Key.
2. Create a text file containing only fictional workshop facts. Select it locally and send `PUT /v1/dossiers/{dossierId}/bestanden/{bestandId}` with one multipart field, `bestand`. Maximum 20 MiB and no text fields. PDF, DOCX, DOC, TXT, MD, PNG and JPEG extensions are supported, subject to byte validation. Save id, filename and sha256. Different bytes or metadata under the same UUID produce a conflict.
3. Optionally request `POST /v1/generatieplan` using only sizes and settings. It reserves no credits. Show the range and uncertainties. Missing amounts in legacy mode do not mean free processing.
4. Choose a new `idempotencyKey` of 16–128 safe characters, such as `synthetic-report-20261004-0001`. Send `POST /v1/rapporten` with dossierId and fictional notes. Save the exact request and key before sending. Save `versie_id` as `versieId`. Acceptance does not prove completion.
5. Read `GET /v1/rapporten/{versieId}` with increasing wait intervals. Only `status: klaar` has a result. Record failures; do not automatically create a new request key to restart them.
6. Read `resultaat.export_url` for PDF, `export_url_docx` for Word and `export_url_pptx` where available. `null` means that format has no file. Download the signed URL within 15 minutes using a separate request without the Rapportly Authorization header. Read the result again for a fresh link. Status access alone does not prove download or content correctness.
7. Read usage and request history again. For the later trial, record acceptance, completion, download, source fidelity, reopening and consumption separately. This guide does not claim those checks have been run.

Offers and presentations use `/v1/offertes` and `/v1/presentaties`, with a new key for each logical request. Both return results through `/v1/rapporten/{versie_id}`. Starting an offer does not send it. Advanced document, language and style options are explicitly partial in this checkpoint; use the documented basic subset for this journey. Field size limits do not override the total JSON-body limit.

## Journey B: save audio, transcribe and create conversation output

1. Choose a recording you made with fictional content. Send `POST /v1/opnames`, multipart `audio`, optionally `titel`, `klant_id` and `duur_seconden`. Maximum 100 MiB. M4A, MP3, WAV, OGG, WEBM and MP4 require matching file contents. Saving requires `bronnen:uploaden`, available audio storage and quota.
2. Save `opname.id` as `opnameId`. Send `POST /v1/opnames/{opnameId}/transcriptie` with a new request key and, for example, `{"model_id":"melia-1","taal":"auto"}`. This requires `transcripties:starten`. Saving and transcribing are separate actions.
3. Save `transcriptie_id` and poll `GET /v1/transcriptie/{id}` until `klaar`. HTTP 200 may also describe a failed job. Keep the actual model identity and usage; do not rely on a fixed price from documentation.
4. Use the checked fictional transcript for `POST /v1/gesprekken`, optionally including `opnameId`. Supply at least one hundred characters, `verwerkingen:starten` and a new request key. Save `gesprek_id` and read `GET /v1/gesprekken/{id}`. Store output text only once complete.

## Journey C: revise a native report and read it back

Read `GET /v1/uitvoer/rapport/{versieId}/rapport_markdown` and save `versie_nummer`. PUT to the same path with `inhoud`, integer `basis_versie` and a new Idempotency-Key of at least 16 characters. Empty text is rejected. This revision path supports the documented native Rapportly report type and requires `documenten:wijzigen`.

Save the returned version number, read current text again and download fresh output files. History is available at `/versies` and `/versies/{nr}`. A deliberate restore uses POST `/herstel/{nr}` with the current `basis_versie` and `bevestigd:true`, creating a new revision. A 409 requires reading again and a deliberate decision on the new base. An old replay must not overwrite a later revision. Paid regeneration is separate; its terminal replay response is still partially documented.

## Connections, mailbox preference and webhooks

The connection guide covers human Google or Microsoft consent. Always read the current `basis` first. Consent and disconnect requests require at least 16 characters in their request key. Completed consent is historical evidence; read current connection state separately. Provider tokens are never returned.

For mailbox preference, read both preference `basis` and the send-capable mailbox's `verbinding_basis`. PUT requires `verbindingen:beheren` and `berichten:verzenden`; it returns historical `ontvangst` and current `huidig`. Choosing a mailbox sends no message. Display `huidig`, including after replay.

For v1 webhooks, register your own public HTTPS receiver using `webhooks:beheren` and explicit confirmation. Keep `signing_secret` secret, verify original bytes and durably deduplicate event IDs. Redelivery must not trigger duplicate work. The webhook guide and signature example explain the receiver responsibilities. Older Zapier events have a separate contract.

## Lost responses and errors

| Situation | Next step |
|---|---|
| Lost start response | Repeat the exact body, object IDs and Idempotency-Key. |
| 400 | Correct invalid input; do not invent a replacement operation automatically. |
| 401/403 | Check environment, user, package and rights; never broaden rights automatically. |
| 402 | Check budget, open reservations and available credits. |
| 404 | Object absent or inaccessible. Do not switch actors to bypass access. |
| 409 on base/version | Read again and require a deliberate new choice before a new request key. |
| 429 | Wait and honour Retry-After. |
| 503 or uncertain outcome | Show no invented success. Retry exactly or read current status. |

Most generic API starts allow 8–128 safe characters; consent and native report revisions require at least 16. These examples use at least 16 throughout. Use a new key for each new logical request and the original key plus original input for a replay. Deleting objects, sending messages, issuing keys and editing existing customer objects are not automatic cleanup steps in this collection.

## Remaining work

The expanded source now includes nested document options, language-variant status, calendar responses, customer folders, recording content, transcript correction, library search, workflow proposals and conversation output. Native and durable replay responses for document starts and report regeneration,  styles/Excel and remaining action-item and subscription groups still need schema work. Three explicit status/content GETs were added selectively later. Direct language starts, policy and model-free recovery are described later as bounded source contracts. Workflow language production was tested locally and in CI and requires complete 376-v2 readiness; separate production behaviour evidence is still needed; secondary-language bulk production remains blocked.

Schema and import validation, synthetic permissions/retry/failure trials, the final technical checks and separately approved publication follow later. Dutch and English become the stable source first; the other 23 active interface languages follow in a separate task.

## Reopen a customer folder and its source

Choose a new UUID for a fictional customer folder. `PUT /v1/klanten/{id}` with `{"naam":"Synthetic team Atlas","externe_referentie":"exercise-atlas"}` is create-only and requires `klanten:beheren` plus Idempotency-Key. Read `GET /v1/klanten/{id}` afterwards and save the exact ETag. A deliberate PATCH such as `{"plaats":"Exercise town"}` needs that ETag in `If-Match` and a new request key. On 412, read again and reconsider the change. Receipt replay must not reverse later changes.

Use the folder ID as `klantId` when starting a document or `klant_id` when saving audio. Find the exercise later with `/v1/bibliotheek/zoek?q=Synthetic&type=alles&limiet=20`. `begrensd:true` identifies a bounded search set; refine the query. A search fragment is not a complete source copy.

Read `/v1/opnames/{id}/inhoud` for the saved transcript and speakers. Correcting a transcript uses a separate `PUT /v1/opnames/{id}/transcript` with four fields: new `transcript`, exact previous `basis_transcript`, new `sprekers` and exact previous `basis_sprekers`. Read again after a lost response. A 409 may indicate a changed base. Correction starts no transcription and does not automatically update generated documents. Download plain text at `/transcript.txt`, or request an audio URL at `/audio`; that audio link lasts 300 seconds.

## Document options, second language and calendar

The production OpenAPI has separate `RapportStart`, `OfferteStart` and `PresentatieStart` schemas. Use only the options for that document type. `documentformat` requires a ready style profile; a presentation uses its own slide layout. Quote lines use integer cents and thousandths plus explicit human confirmation. Server confirmations for catalog or route lines belong to the same user, organization and concept. Do not invent a confirmation token.

A conversation may include language choices at initial creation. The old paid conversation language-variant POST still exists in source, but lacks the complete new key/budget/job contract. That old route is therefore not a start example in this collection. The separate new bound language starts, policy changes and retries are described later as source contracts; they still require explicit release approval and integrated verification. This guide does not remove a runtime route or prove a live deployment change.

`GET /v1/agenda/afspraken?uren=24` reads available appointments over 1–72 hours with `agenda:lezen`. `verlopen` lists expired provider connections. Partial provider success does not prove that all calendars were read completely.

## Workflow proposal through submission

1. Read `/v1/keuzes/uitleg` and optionally request model-free advice using `POST /v1/keuzes/advies` with `{"documenttype":"rapport","complexiteit":"hoog"}`. Save `advies_id` and `advies_hash`. Relative costs are not a fixed credit price.
2. Deliberately create a fictional proposal with `POST /v1/workflowvoorstellen`, for example `{"naam":"Atlas exercise process","klantbeschrijving":"Fictional process: collect sources, draft, review internally and save after approval.","gekoppelde_stijlprofiel_ids":[],"deling_scope":"persoonlijk"}`. This may consume model credits. Save the proposal ID and ETag.
3. After losing the response, read `/v1/workflowvoorstellen/status` with the original Idempotency-Key. `veilig_opnieuw:false` does not establish that a new paid start is safe. Display the uncertain state and retry the same logical request deliberately.
4. Read the proposal. Answer only known question IDs with PATCH, the current If-Match and a new request key. Send `aangepast_procesvoorstel` only when deliberately replacing the process settings.
5. Only `klaar_voor_indienen` with at least one process step can be submitted through `/indienen`. Submission proves neither human approval nor publication. Read the current state afterwards.

Workflow lists currently return at most the first 100 items; the storage adapter does not process a continuation cursor. A cursor field does not establish complete pagination.

## Human follow-up

`POST /v1/zelfservice-aanvragen` with `{"doel":"abonnement","bevestigd":true}` creates a confirmation entry valid for fifteen minutes. The user opens the returned Rapportly URL and signs in with the same identity. At most ten requests remain open. `geopend` only records opening that entry; `uitvoering_bevestigd` remains `false`. This request records no purchase, payment, subscription change, key issuance or MFA change.

## Account, team and key management

Read the six settings under `/v1/account`: `bedrijfsgegevens`, `schermtaal`, `uitvoertalen`, `teamkwaliteit`, `mijnkwaliteit` and `teamrollen`. Preserve `basis` literally as a string; `"0"` is valid too. PATCH requires `basis`, `waarde`, `bevestigd:true` and a fixed Idempotency-Key of 8–128 characters. For example, `{"basis":"0","waarde":{"taal":"nl"},"bevestigd":true}` changes the screen language only if the basis just read was actually `"0"`.

The response separates `ontvangst` (the historical receipt) from `huidig` (current settings). A replay does not undo a later change. Read and reconsider after a basis conflict. Team quality, output languages and team roles require the owner; personal quality cannot override fixed team policy. `/v1/team` reads roles and capacity without sending an invitation or ordering a seat.

`/v1/koppelingen/sleutels` and its related GET routes expose metadata only, with `sleutels:lezen`. Issuance, activation, policy changes and revocation require a user session on `/koppelingen`, outside `/v1`. A delegated key becomes usable only after its assigned user activates it. Secrets are shown once. Never blindly repeat issuance or activation after a lost response. In full API mode the old `/rechten` route returns 409; use current policy and If-Match.

## Customer-specific intake address

1. Read `/v1/klant-inname` for current organisation availability. A read failure is 503, not evidence that intake is disabled.
2. Read `/v1/klanten/{id}/inname` with `klanten:delen`. Preserve the UUID in `basis`, including when `actief:false`.
3. Deliberately POST to the same path with `{"basis":"<UUID just read>","actief":true,"bevestigd":true}` and a unique operation key of 16–128 characters. Use only a fictional customer folder in an approved exercise environment.
4. Read `ontvangst`, `herhaald` and `huidig` separately. The address in `huidig` is current. A new activation rotates the old address; an exact retry does not rotate it again. `actief:false` revokes the current address. An inactive-active-inactive cycle changes the basis; disabling an already inactive address does not.

This address operation starts no model processing and sends no portal invitation. Organisation sender rules and further portal actions have separate contracts; open schemas remain listed in the inventory.

## Recording workspace, transcription preferences and CRM

Read `/v1/opnamewerkplek/mogelijkheden` before choosing transcription options. `automatisch` selects melia-1 for basic transcription and enhanced for vocabulary, voice profiles or voice enrolment. Explicit melia-1 cannot be combined with those richer options. Only the owner can change recording options while the recording is saved or failed.

Workspace settings and personal transcription preferences each have their own `versie`; transcript history uses `transcript_revisie`. Read the correct basis, change deliberately and read again after a lost response. Deleting a recording folder preserves recordings. Recovery metadata exposes only ID and creation date of owned recordings with lost customer access; content remains protected until an allowed unlink operation.

A voice profile requires explicit consent for the user's own voice with current `toestemming_versie`. Creation uses a repeatable operation key; update/delete requires the exact current `bijgewerkt_op` in If-Match. Creation alone does not make the profile ready for recognition. Provider biometric identifiers are never returned. The example collection does not register voices.

CRM uses GET/PUT `/v1/klanten/{id}/crm`. PUT replaces exactly one complete section (`opvolging`, `contactpersonen`, `notities`, `checklist`, `pins` or `definitief`) using current `revisie`. Preserve existing row IDs and retain rows you do not intend to remove from a full section replacement. At most one primary contact is allowed; received/checked checklist rows require an accessible source file. Note authorship and dates are maintained by the server. After a lost response read CRM again; a new revision does not make blind retry safe.

For search, first read `/zoekbestanden`. Deliberately POST `/zoekbestanden/{bestandId}` with `soort` bestand or versie to extract existing text without model generation. HTTP 200 may contain `wacht`, `bezig`, `mislukt` or `onleesbaar`. Scanned document OCR is not promised. Then use `/zoeken?q=Atlas&offset=0`, at most thirty results per page, following `meer`. An internal result path is not standalone download authorisation.

## Access, links and briefing

Read `/v1/klanten/{id}/toegang`; only when `kanWijzigen:true`, use its current revision for a deliberate team, selected-colleagues or private choice. Selected access requires other permitted colleagues; team/private uses an empty selection. The revision prevents an old choice from overwriting newer permissions.

`POST /v1/klantmappen/koppel` accepts one source item (recording, conversation or dossier) and exactly one existing customer ID or customer name. Source material stays in the same storage and library. Read the current link after an unclear response; this route has no idempotent receipt contract.

A report preference belongs to your user and the report version’s style profile. Read `/v1/rapporten/{versieId}/voorkeuren` and send the JSON ETag literally in If-Match with PUT `tekst` (at most 2000 characters). This does not rewrite an existing report.

Share and intake links require deliberate confirmation, 7, 14 or 30 days of validity and a fixed operation key. An active receipt returns an access-bearing token/URL; store it securely. An exact old request for an expired or revoked link returns metadata only, never extends it and creates no new token. Lists do not expose tokens. Revocation is separate, and already revoked links return 404. Creating a link sends no message.

A briefing start requires the current Amsterdam date in `dag`, for example `{"dag":"<current day>","taal":"en","vernieuwen":false}`. The API uses durable paid processing; an old date returns 409. A completed daily result may return from cache immediately; 202 is not completion. Read GET on the same customer path. Renewal deliberately starts new generation and must not be an automatic response to uncertainty.

Full customer deletion has a separate administrator preflight with counts, blockers and a fingerprint. The mutation requires the exact current name, fingerprint and confirmation; 202 means storage cleanup is still pending. This differs from deleting only the folder. The collection contains no executable full-deletion example.

## Classic style source, OCR and recovery

Read `/v1/stijlprofielen/{id}/bronstatus` with `stijlen:bronnen_lezen`. Preserve current `basis`; the list exposes source metadata without content or storage paths. Only the current creator and owner can start recovery or OCR. A team role does not permit impersonating another creator.

When `ocr_keuze_nodig:true`, POST `/ocr-schattingen` with `basis`, `bevestigd:true` and a fixed operation key to request a quote. This also requires `stijlen:uitgebreid_lezen`. Quoting explicitly contacts the provider to count PDF usage and remains subject to privacy consent. The receipt lasts fifteen minutes; an uncertain claim must not automatically contact the provider again. `verwerking_gestart:false` means paid OCR has not started.

After deliberate review, start `/uitgebreid-lezen` with current basis, `schatting_id`, confirmation and a new logical operation key. General `/herstellen` uses `stijlen:herstellen`, the same source-read permission and a supported terminal native failure. Then read `/herstelopdrachten/{aanvraagId}`. An accepted job or successful worker is not necessarily an active document style: existing checks and approval may still be pending. Replaying does not enqueue another job; original receipt basis and current source basis remain distinct.

## Mail preview, approval and uncertain sending

1. POST `/v1/gesprekken/{id}/mail/voorbeeld` with `mailsoort` klantmail or followup, the intended recipient and optionally an existing language variant. This neither queues nor sends a message.
2. Review subject, text, attachments, sender, recipient and sending policy. Followup also returns `basis`. Optional text editing belongs to that basis and cannot simultaneously replace a translated variant. Preserve exact preview input and `review_hash`.
3. Only after approval call `/mail/goedkeuren` with `bevestigd:true`, the hash and the current basis for either mail type. The route uses a review/snapshot receipt, not a free Idempotency-Key. Stale content or sending details require a new preview and approval.
4. Read `/v1/mail-outbox`. `queued` may refer to a future sending window. MIME acceptance by Gmail/Microsoft, `sent` and `handmatig_bevestigd` do not prove delivery. Delivery/open/click fields may be absent; a read failure returns 503.
5. With `directe_status:onzeker`, automatic resend stays disabled. After at least five minutes, the owner can use current `poging_id` to deliberately select `stoppen`, `als_verzonden` or `opnieuw` on `/mail-outbox/{outboxId}/herstel`. Resending also requires `dubbelrisico:true`: the original attempt may already have arrived. Repeating the same recovery decision does not create a second action.

## Expired trial

Reading remains possible for an authorised user/key, as do explicit read actions such as mail preview. New work requires valid paid continuation; otherwise 402 `rapportly_proefperiode_verlopen`. An unreadable check source returns 503. The bounded exceptions are a human self-service entry, disconnecting a provider, revoking a webhook and stopping/manually confirming uncertain mail. Resending is not exempt. All other permission and object checks still apply; deletion is not generally exempt.

## Style intake, metadata and review

Upload a classic style using 1–3 files, a name, type, deling_scope, the text value bevestigd=true and a stable request key. Replays retain exactly the same bytes, names, ordering and metadata. An incomplete receipt can be resumed for thirty days; a complete receipt has a request ID. Read the key-bound `/v1/stijlprofiel-uploads/{id}` to follow receipt status. The older `/stijlprofielen/aanvragen/{aanvraagId}` handles native UUIDv4 requests; new API uploads use their own UUIDv5. Receipt and processing do not establish approved publication.

GET `/v1/stijlprofielen/{id}` returns metadata and an ETag. PATCH and deactivation require the exact current ETag and current owner/team permissions. Read current state after losing a response. GET `/template-navigatie` returns only relative review rectangles and page numbers, without text. Detailed review, fixed text, form choices and publication are described separately below.

## Balance, storage and personal usage

GET `/v1/saldo` returns the current personal or organisation balance. GET `/v1/opslag` returns byte counts and current extension prices without placing an order. Incomplete personal attribution returns `null` for used and available bytes; treat these values as unknown. Existing organisation extensions may need further reconciliation.

GET `/v1/credits/verbruik?maand=2026-10` selects a UTC month; omitting the month selects the current UTC month. The overview adds exact final ledger charges and reports any advances. The outstanding advance total is current, whereas repayments belong to the selected month. Excessive or incomplete source records return 503 instead of an incomplete total.

GET `/v1/credits/runs/{runRef}` is a separate historical run overview. Its rounding can differ from the monthly ledger sum. `afgerond:true` means no credit reservation remains open; it does not establish successful document generation or email delivery. Completion notices contain no content and refer to the actor’s successful jobs with proven internal cost coverage.

The older `/instellingen/taal` read route falls back to Dutch on a read failure. Use the explicit account contract and revision basis for changes. Subscription and document bank reads appear below; dynamic package feature semantics remain open.

## Document review and presentation corrections

GET `/v1/stijlprofielen/{id}/template-controle` returns the current review. A Word or PDF document has a `sha256`, document preview and brand assets. Depending on the source, the response also includes font substitutions, preserved formatting, fixed PDF text or form adjustments. Review the complete preview; temporary links are not permanent public files.

Select fixed PDF text only from existing candidates using `/template-vaste-teksten`. Form choices use `/template-formulier-aanpassingen`: existing choice IDs map to true or false. Both routes replace the review and hash without publishing. Document approval uses `/template-goedkeuren` with the current `controleSha256`, the complete `goedgekeurdeMerkassetSha256` list and `bevestigd:true`. Forms also require `formulierInhoudGecontroleerd:true`. Missing or stale review requires a new review.

Presentation inspection belongs to an already published style. Its response has `sha256:null`, the active `profielversie_id`, slides, layouts, fonts, assets and measured fidelity. The approval route uses `basis_versie_id` here. Reconfirm the current version or create a new version using `correcties.assets_uit` and `correcties.layoutrol`. Checking the active version prevents a stale choice from overwriting newer work. Read the current version after losing a response.

## Reading subscriptions and the document bank

GET `/v1/abonnement` accepts no query parameters and requires `abonnement:lezen`. It describes package, balance, payment/setup status, change permissions, privacy basis, trial period and White Label separately. Known payment details do not establish successful payment. A trial banner is not an access gate. Missing supporting status may be null or unavailable. Package features pass through as dynamic database JSON; that field remains explicitly marked as an open semantic schema.

This read route does not change a subscription. Use the previously described self-service request for a deliberate human follow-up. Opening that page does not establish payment or a completed change.

GET `/v1/documenten` returns active document bank entries for the current organisation. It collects all internal pages and has no external cursor. `/v1/documenten/{id}/download` returns a URL valid for five minutes; a previously deactivated document may remain accessible for an older conversation bundle. Do not store or log that temporary URL as a permanent download link. These contracts do not expose document bank upload or deactivation as external API routes.

## Style research and targeted refinement

GET `/v1/stijlprofielen/{id}/onderzoek` lists the actor’s latest one hundred research notices. A discovered technical recipe is not an active style. Create a concrete proposal with `/onderzoek/{meldingId}/voorstel`, review it and deliberately apply it with the correct `basisVersieId` and `bevestigd:true`. Presentation research requires the same actor to open the exact PDF preview first; that GET records the reviewed hash. Read again when the basis changes.

Postponing a notice or keeping the existing style uses `/keuze` with `later` or `behouden` and the exact received `bijgewerktOp`. Do not reformat the timestamp. Proposal, application and choice share a limit of twelve mutations per minute. `klantCredits:0` applies to these research recovery routes. Excel has corresponding read, choice and apply routes, while its nested research proposal schema remains open in the inventory.

A paid custom formatting change uses `/v1/stijlprofielen/{id}/verfijnen/jobs`. It supports the existing legacy engine `profiel_v1` only. Submit the current `verwachte_profielversie_id`, a short formatting request, `bevestigd:true` and a stable request key. For example: “Increase the spacing.” Do not include customer content, contact details or amounts.

The external API accepts an 8–128 character safe request key; its adapter transforms that key before the native refinement handler receives it. Keep `aanvraagId` and read `statusUrl` with the same API key. A replay of an already terminal receipt can still return 202. The receipt expires after 48 hours. `controle_nodig` indicates uncertain processing that needs review of the current style; do not automatically start another paid request. The collection provides a blocked synthetic start and a separate status read without an automatic chain.


## Style workspace: propose, inspect and explicitly apply

The source contract now describes capabilities and active basis, durable proposals and receipt recovery, section and image inventory, previews, activation, proposal deletion and version restoration. This checkpoint uses `66d3dcaf`; availability and behaviour have not been tested.

1. Read `/v1/stijlprofielen/{id}/werkplaats`. Use its current `basisVersieId`, offered fonts and capabilities; support depends on engine and permissions.
2. Start `/voorstellen/jobs` with a fresh idempotency key, `basis_versie_id` and one operation: `opmerking`, `instellingen`, `afbeeldingen` or `indeling`. Only the comment operation may use AI/customer credits. The collection includes a blocked local settings example with font size11; consciously select a real change from your synthetic basis.202 confirms receipt, not a completed proposal.
3. Read status by request ID. Status is scoped to actor/organisation/profile; receipt lookup with the original idempotency key additionally depends on the original API key. Receipts expire after7days. Do not automate a new paid attempt after `controle_nodig`.
4. Open the specific preview deliberately. This GET may schedule rendering and initially return202 with `Retry-After:2`. The final PDF carries `X-Rapportly-Preview-Hash`. The collection therefore does not automatically run previews, activation or restoration.
5. Activate using the current basis, `bevestigd:true` and that exact hash. Read the active version after a lost response. Restoring an earlier version requires a preview opened by the same API key during the current UTC day; reopen it after the day changes.

Section proposals retain every visible section. Applying a derived source copy creates a legacy profile without exact tables, source images or fixed source formatting. Image replacement uses a current inventory key and an owned image-library ID, never caller-provided storage paths. These descriptions do not establish a live release.


## Organisation intake: policy and receipt

The selective update from `354bfa39` describes GET and POST `/v1/klant-inname`. Reading now includes a persistent `basis`, `is_beheerder` and `versiecontrole:true`. Preserve the basis exactly. Management requires the actual organisation owner, `teaminstellingen:wijzigen`, current package access and a16–128character idempotency key.

The body contains `actief`, `basis`, `bevestigd:true` and an optional `allowlist`. Omitting the list preserves it; an empty list removes additional senders. The customer's own email remains allowed through the existing customer rule. Up to20input entries are trimmed, lowercased and deduplicated. Any invalid entry rejects the complete change.

The response separates `ontvangst` (historical ID, basis and active state), `herhaald` and `huidig` (current policy). An exact old replay does not apply the change again. Reusing the key with changed input returns409. A timestamp-only update does not change the basis. This route creates no organisation address and starts no AI or credit processing; customer addresses remain separate. Blocked collection examples use a fictional sender and `actief:false`. Deliberately sending that example still changes real policy.

This selective addition has its own commit references and file hashes. Contracts awaiting handoff have not been silently included; final verification and publication remain open.

The selective trial gate from `b7742838` also permits portal revocation, revocation of a portal release and pausing/cancelling a workflow run after trial expiry. Current actor, object and API-key permissions remain required. This exception does not allow new workflows, new publication or restarting work; it does not document the new runner contracts.


## Model-free style maker

The six external style-maker routes provide durable proposal and example jobs, receipt lookup, status, result retrieval and cancellation of an incomplete upload. Starts use multipart. Example jobs send `ontwerp` as a JSONstring in one text field and accept no files; a PNGlogo may be embedded in the design. This does not publish an active style.

Use a fresh8–128character key starting with a letter/digit, followed by letters, digits, dots, underscores or hyphens. The general API colon allowance does not apply here. Reuse the exact body after a lost response and look up the original receipt. Receipts and results expire after48hours. Status by request ID is actor-scoped; lookup using the original key also depends on the original API key.

A proposal result contains three designs. A document example always returns JSON with base64 `bestand`, `mime` and `ext`: DOCX, PPTX, XLSX or PNG for house-style output. Result metadata may retain raw design input while rendering normalises colours and images. Result retrieval returns503 until the job is ready.

No paid model is used, although an optional website can trigger a restricted network read. The collection omits websites and files and blocks starts by default. Aborting only applies to an incomplete upload with no scheduled job; it does not cancel received, running or completed work. No imports or API requests have been executed.


## Source profiles and source documents

A source profile defines the fields to read from recurring documents. Contracts cover list, detail, creation, editing, deactivation, file links, extraction starts and human review. The canonical profile PATCHbody is usable; broader historical input coercion remains explicitly open compatibility work.

On creation, `velden` is a JSONstring in multipart. Supplying usable fields returns201 without model inference. Without fields, an instruction or instruction file is required and the response is202. An exact replay returns200 with current status. PATCHuses a real JSONarray instead. Send `If-Match` containing the quoted integer `versie` read from the profile; the GETdoes not itself set an ETag. Deactivation does not delete historical files.

Extraction starts reference an existing customer `dossier_bestand_id` and an optional `bronprofiel_id`. There is no extra external source-document upload route. Another start cannot adopt or charge the same file again. Follow progress through the customer's source-document list. Field inference and extraction may use customer credits.

Approval or rejection requires `te_controleren` and the exact quoted `bijgewerkt_op` in `If-Match`. Omitting `resultaat` preserves existing results. Supplying it replaces the entire list. The API restricts entries to five keys but this handler does not further type-check confidence, page or field key; deliberately supply the existing key, text/null and confidence0–1. Receipt does not prove extraction accuracy.

The collection only adds reads and a blocked fictional profile with a manually defined field. No paid extraction or approval starts automatically.


## House-style templates by document type

Eight source contracts cover quotations, reports, email and conversation reports: reading, upload, receipt lookup, preview links, approval, brand fonts, colour/font adjustments and covers. Uploads may use paid vision. Font, adjustment and cover requests create a new draft with local rendering. A202start always says `bezig`, including replays; read the receipt for actual progress.

The original idempotency key uses8–128characters without colons. Temporary input and the basis snapshot have7day retention; replaying a start afterwards returns409. Technical receipts may remain readable. The newest draft need not be the active style. The selectively included read fix61b786b3 returns503 sjabloon_onbeschikbaar for template or version query errors. Only a genuinely absent template returns200 with null.

Preview returns a300second signedURL and `preview_hash`, immediately recording the current actor as the latest viewer. This proves the link request, not a PDFdownload or human assessment. Approval requires that actor, the exact hash and `bevestigd:true`; the saved active basis is checked again. Read status after a lost response. The collection does not automatically request previews or approve.

Brand fonts require explicit licence confirmation and at most2MiB WOFF2/WOFF/TTF/OTF. Embedding only applies to PDF. CoverHTML is limited to32000characters and sanitised; an empty string removes the cover. Canonical colour/font input is documented while broader historical tolerant input remains marked partial. No publication or runtime verification has been performed.

## Reading existing language variants

The selective source `99c1da9a` adds three GETcontracts: conversation status, report status and a specific language outcome. Status includes requested choices, resolved languages, active selection and history; policy fields may be absent in the documented native fallback. History omits old bundled-credit amounts. Use your usage overview for current cost accounting.

Check `contentAvailable` per outcome. Content retrieval returns only `language`, `outcome`, text/null and `artifactId`. Storage paths and `artifactRefs` are excluded; documents use existing authenticated result downloads with current object permissions. Source and actor permissions are checked before and after reading. Not ready409, invisible404 and unreliable source503. No query parameters are allowed.

The collection adds only three read requests. Paid language starts, policy and retry are not presented as completed parts of the full new API. The old route remains in source, while publication awaits safe replacement and testing.

## Styles v2: rules, versions and a paid sample

Read `/v1/stijlen-v2` and `/v1/stijlen-v2/context?document_type=rapport`. Personal styles are visible only to their owner. Updating organisation or document styles also requires current team publishing permission. Context resolves rule priority as protected, managed professional style, organisation, document, personal and request. A higher layer wins for the same rule key.

For a fictional exercise, create a personal style using a new idempotency key:

```json
{"naam":"Oefenstijl Atlas","laag":"persoonlijk","bron":"begeleid","antwoorden":{"toon":"zakelijk","aanspreekvorm":"je"},"regels":[]}
```

Creation and version publication do not call a model. `201` means new and `200` repeats a receipt. Save `ontvangst.stijl_id` and `ontvangst.versie_id`; the returned `stijl` always describes the current active version. Publishing through `/{id}/versies` requires the previously read `basis_versie_id`, name and rules and immediately activates the new version. A repeat returns the original version alongside the current style; avoid overwriting newer choices. Use a new request key for each new operation. Restoring an existing version separately requires its `versie_id`, current basis and `bevestigd:true`.

A style test through `/{id}/test-aanvragen` is a paid production action subject to credits, budget and current permissions. Supply fictional sample text of20–3000characters or one existing visible completed conversation. Do not supply `aanvraag_id`: the API derives it from the key. After202, read your `/test-aanvragen/{jobId}`; waiting does not require another start. The worker limits attempts to ten per actor and UTC day. Results contain text, applied rules, version IDs and any source truncation warnings. A successful result returns410 from30days after job creation. Uncertain provider contact does not authorise an automatic new attempt.

Feedback is allowed once per own test: score1–5 and an optional comment of up to500characters. The server stores only a hash of the comment. Repeating feedback returns 404; this does not authorise a new test. `POST /{id}/analyse` reads existing analysis without starting AI, but has no separate trial-period exemption in the action catalogue.

Nested request rules in context queries and historical tolerant normalisation remain explicitly partial contracts. Native administration and synchronous test routes are not part of this external API catalogue. These are source contracts; imports and execution have not been tested.

For these three language GETs, the server rechecks the key, activation, pause, expiry, rights/ceiling, actor role, organisation and package immediately before responding. Revocation during the read returns403 api_leesdelegatie_geweigerd; an unreadable check source returns503.

## Follow email notifications

The webhook guide now also describes `mail.geaccepteerd`, `mail.onzeker`, `mail.mislukt` and `mail.afgeleverd`, using an outbox and attempt reference. Provider acceptance is not delivery evidence. Only a signed Resend receipt matching the correct email can produce `mail.afgeleverd`; manual confirmation and Gmail/M365 do not. Use the accompanying Node.js receiver function and persist event ids durably. Source3897cbd9; not executed or published.

## Choose a document format without generation

Read `GET /v1/documentarchetypen` for the four universal document formats and their relative sample filenames. Presentations have a separate registry at `GET /v1/presentatie-archetypen`. The `vrijgegeven` field is a feature flag; actual generation still depends on current permissions and release gates.

Read `GET /v1/documentarchetypen/voorkeur` for the organisation preference and retain the complete quoted ETag. Within this external API, only the organisation owner may change it through PUT with `If-Match`, `registryVersie: rapportly-archetypen-v1` and an `archetypeKey` from the registry. `null` restores automatic selection. Read again after409 or a lost response. This route has no separate Idempotency-Key contract.

`POST /v1/documentarchetypen/selecties/controleren` reads which selection applies. Explicit universal selection returns the selected format. Automatic selection first uses the organisation preference, then a style signal, then the document type. This stores nothing and generates no document. The collection includes a fictional example and also blocks this POST until you deliberately enable requests.

## Prepare and fill Excel safely

The base routes describe availability, source list, upload, download, page images, image choices, review, preview, approval, filling, recovery and deletion. The object read model with historical analyses and separate paid source proposals is described in the following chapters. Execution and import validation remain open.

Every Excel mutation requires `X-Rapportly-Client-Version`; missing or invalid values return426. Use your released client version identifier. Upload one local fictional xlsx/xlsm/xls file up to25MiB with a name and mode `sjabloon` or `aanvullen`. Retain the receipt id.201proves storage; a source with a research issue may still be unsuitable for filling.

Review and filling accept up to16MiB of uncompressed JSON. Confirm every cell as fixed, input, calculation or empty; formulas remain protected. Fixed and, where present, passive parts require confirmation. Create a synthetic preview, inspect its file and only then approve using the review object's `inhoudHash` and the preview's `bestandHash`. These hashes are distinct.

Filling stores immutable input and starts a native calculation job.202can indicate waiting or an existing completed result; retain the input id and read current status. Ordinary filling starts no AI and charges no new credits. AI source proposals have a separate paid start and human review. Recovery reuses active jobs.

Webhook pagination is selectively corrected from source2779d55: decimal offset0–10000only, without leading zeros, exponent notation or repeated query values. The next offset remains within10000 or becomes null. Functional tests remain outstanding.

## Propose Excel values from selected sources

First read `/v1/excel/bronvoorstellen/status`. When `beschikbaar:false`, `code` explains why the domain gate is unavailable. Preparation requires an existing review approved by a person. Select at most ten sources combined: your visible recordings, up to five PDF/DOCX/TXT/MD files and optionally pasted text with a label. Combined text is limited to 120,000 characters. PDF is read locally without OCR; an empty or unreadable page is rejected.

`POST /v1/excel/{id}/bronvoorstellen` stores the selected sources and returns a proposal id. This does not start a paid model. Only `POST /v1/excel/bronvoorstellen/{voorstelId}/start` with `bevestigd:true` starts paid source extraction. Both steps require the client version header; only preparation uses your Idempotency-Key. Retain the proposal id after uncertainty and read its status before proceeding.

The read response contains source fragments, candidates and literal evidence. Treat this as content, not log metadata. Each field may be found, missing, conflicting or invalid. Conflicts have no automatically selected winner. Credits and the proposal hash appear only when complete. Review every value; filling is a separate step using the same review, proposal id and `voorstellenGecontroleerd:true`. A changed value remains identifiable as a manual choice.

## Read an Excel object and its analysis

`GET /v1/excel/{id}` now describes content for all six object types: source, review, preview, approval, input and result. `versies` contains up to 200 direct children. Only input may include current job progress. Storage references are omitted. `blad.pad` in analysis names a part inside the workbook, not server storage.

The schema includes cell values, protected formulas, editability, native validation, fixed and passive parts, and diagnostics. Source analysis may be null while an existing research issue needs resolving. The research proposal route now also describes the replacement analysis; review it before explicitly applying it. This recovery research charges no customer credits.

Paid source extraction does not repeat an uncertain previous provider call. A stored answer may still await settlement. Only completion exposes final integer credits; proven internal coverage can make these zero. This does not mean there were no provider costs. This object flow is documented source; execution, files and failure paths have not been tested yet.

## First customer email with recorded approval

The selective source checkpoint `48512877` gives the first customer email the same controlled process. Request `/mail/voorbeeld` with `mailsoort: klantmail` first. A free-form `mail` field is forbidden here: the preview uses the saved customer email or an existing customerEmail language variant. The response includes `versiecontrole:true`, `mailsoort`, `basis`, `review_hash`, `afzender` and the current `outbox` or null. A missing required sender or attachment causes an error. Read the separate outbox route for existing recovery status in that case.

Review the exact recipient, sender, text and attachments. Approval requires `basis`, `review_hash` and `bevestigd:true` for either mail type. Copy these from the same preview and preserve the other request fields. The collection uses empty basis/hash values, an invalid example address and blocked mutations; nothing has been sent. A real sending trial later requires a deliberately selected test address you control in an approved environment.

An exact retry returns the original outbox ID and its current status. That receipt may omit `wacht_tot`. First customer email and followup each have their own receipt. This does not resubmit a previously sent, processing or uncertain email. `kan_goedkeuren` only describes a row never offered to a provider; `herstel_mogelijk` belongs to the current actor and the same mail type. An uncertain legacy row without a snapshot returns `mail_legacy_herstel_verplicht`. This does not authorise a blind resend.

Approval binds the exact bytes of at most 100 unique attachments totalling 15 MiB, along with the sender and content. The sending attempt rechecks permissions and those same values. Provider acceptance, manual confirmation and actual delivery remain separate states. Only the documented signed delivery event can prove Resend delivery. Build status: written, without an executed sending trial or a live availability claim.

## Workflow run: receipt, human review and actual production

The eight documented runner actions use source `cd6dcaf4`. An execution version and a production run are different objects. POST `/v1/uitvoeringen/{id}/productieruns` records a run against the current review basis. A new run awaits human source review. `bevestigd:true` in an API request does not replace that content review. An authorised signed-in user must use the native interface. The API has no confirmation route.

GET `/v1/workflowruns/{runId}` returns current run status, basis, version and the bound main/render job. For a deliberate mutation use that exact basis and version with a stable 16–128 character safe Idempotency-Key. A historical replay receipt does not establish current progress; read the run again. The start response is HTTP200 and does not promise that a provider has run. A new request key does not create a second run for the same execution version.

Execute, pause, resume, recover and cancel each have their own state requirements. Pause blocks further new steps at the next control gate; an already running provider may still incur costs. Resume retains the same job and attempt counter. `herstel_zonder_provider` uses only proven saved content and refuses new provider costs; missing cache remains an error. `offerte_afmaken_mogelijk` only completes existing quote metadata and number receipts. An uncertain provider outcome does not authorise automatic restart. Pause and cancel remain available after trial expiry, subject to the other permission checks.

Download only once the run is canonically complete. `/download/pdf`, `/download/docx` and `/download/pptx` for a bound presentation return a link valid for 60 seconds. Download separately without the API key. PDF/Word follow later native revisions; revised text does not imply a new PowerPoint. Runtime release and full API release must each be enabled separately. This checkpoint enables neither. Production context with its complete snapshot, execution versions and quote rule receipt import receive their own subsequent schemas; the inventory keeps them open.

## Quote rule receipts before production

POST `/v1/uitvoeringen/{id}/offerte-regelbewijzen` imports an actual, still valid native catalogue or route confirmation. The body contains only schema2 `offerte_invoer`, including concept ID, subject, conditions and at most 50 lines. At least one line must be a confirmed catalogue/route line; fully priced manual lines may accompany it. An entirely manual quote uses the direct production choice. Import does not calculate a new route, start a provider or claim a quote number.

Use the current execution ETag as `If-Match` and a stable 16–128 character safe Idempotency-Key. Explicit `offertes:regelbewijzen` permission is required alongside read, workflow management and processing start permissions, in both current rights and the maximum grant. At most 100 imports are allowed per execution, only before any production run exists. Serialized JSON input is limited to 200,000 characters. The existing native validator checks price, quantity, VAT, validity and current catalogue/workflow/route versions.

The response contains receipt ID, receipt hash, execution, base version, validated lines and native totals. Money uses integer cents and quantities use thousandths. These are quoted amounts, not customer credits or provider costs. Signed confirmation tokens and their raw contents are not returned. The collection leaves token, concept, execution and ETag blank; later provide only a current test confirmation belonging to you and do not log it.

After a lost response, repeat the same body, ETag and request key. A proven receipt can be replayed after token expiry; a new import of an expired token is refused. The same actor, original key, execution, concept and current access rights remain mandatory. Then use the receipt ID as `productiekeuze.offerte_regelbewijs_id`, without also supplying `offerte_invoer`. The next execution version and human production review must retain that exact calculation. The fully nested execution/production context is still being documented separately; this import has been written and has not been executed.

## Execution versions and current production context

Source `579b18e8` adds the current execution list, version history, read models, context creation, instruction changes, source checks, source confirmations and phase transitions. GET `/v1/uitvoeringen` returns at most 50 metadata items per page; use its returned cursor. GET `/v1/uitvoeringen/{id}/versies` uses a separate positive version number cursor and returns at most 50 earlier versions. An execution phase does not prove that a document producer has run.

Context creation uses an existing completed Rapportly report and returns HTTP201. Save execution ID and ETag. Instruction changes and source actions require the exact current `If-Match` and a stable request key. After a lost response repeat the exact same body and ETag; after a genuine conflict read again first. The context retains the instruction, source manifest and hashes, style/workflow versions, optional organisation instruction and production choice. A historical receipt and the current version remain separate.

Source checks use only current saved sources, answers and selected source versions. Each answers/source-choice map allows at most 100 keys and 20,000 UTF8 bytes. A changed source basis prevents saving against an old review. A separate source confirmation records one precisely checked point; it does not replace human approval of a production run. Phases only change through documented transitions. Moving to definitive requires a verified report version and does not itself produce or send anything.

GET `/v1/uitvoeringen/{id}/productie` shows the current snapshot, generation choice, any source block and the user's own run. A block makes the start basis null. Historical `prijsgevolg` is a report-context estimate, not a current quote/presentation price or booked cost. Quote totals come from `offerte_snapshot.calculation`; actual credits and provider costs retain their own accounting process.

Only reports accept `productiekeuze:{documenttype:'rapport',begeleidende_brief:true}`. A form source refuses that cover-letter option. The letter is a bound step within the same native report production and does not send email; `generatiekeuze.begeleidendeBrief` shows the choice. Presentations and quotes have their own explicit style/content. `taalvervolg` remains false in the original production context; a later translation uses the separate language production routes below. Neutral style and canonical quote receipts are described; engine-specific target identities and historical retention/approval variants keep the relevant routes partial. No final validation or execution has taken place.

## Compare consecutive report revisions

GET `/v1/uitvoeringen/{id}/verschillen` uses the connected source `af35221c` plus `9bab4bc9`. Send no query parameters; any parameter results in HTTP 400. This replaces the earlier documentation that the adapter was missing. The implementation is written but has not been tested or published.

The newest run belonging to the caller for this execution must itself be complete and produce a report. A newer unfinished run does not fall back to an older completed run. Both the saved workflow profile and the current active, approved and available profile version must explicitly allow full internal comparison. Current key, actor, organisation, source and report permissions still apply.

The route compares only the current saved report Markdown revision and its immediate predecessor. Each complete revision is limited to 2 MiB of UTF-8 content. An available result contains `uitvoering_id`, `run_id`, `rapport_versie_id`, `van_versie` and `naar_versie`. Equal content gives `gewijzigd:false` and `blok:null`. Changed content produces one block from the first to the last changed line; unchanged lines between those changes may be included. Each side contains at most 8,000 Unicode code points, with `ingekort` indicating truncation. On an empty side, the ending line may be lower than the starting line.

A result with `beschikbaar:false` gives an explicit reason for disabled policy, an unfinished result, an unsupported document type, no previous revision, missing or oversized history, or changed history. Permissions, policy, run, pointer and history are read again before returning content. Conflicting current state can give HTTP 409; read or hash failures give HTTP 503. Responses use private no-store caching. This does not compare letters, source manifests, formatting, other document types or execution snapshots, and it does not accept arbitrary revision numbers. No provider work or mutation is started.

## Save the first customer email as a mailbox draft

Source `c141e025` defines four separate routes. Read GET `/v1/gesprekken/{id}/mail/concepten` with `berichten:verzenden` first and retain the current `basis`. The response includes up to 100 receipts belonging to the original key and actor. The native legacy fallback is not an API response.

Choose Outlook or Gmail explicitly. POST to the same route with `/outlook` or `/gmail` and `{aanvraag_id,basis,bevestigd:true}`. Create and retain one request UUID for that exact input; optional `language` selects an existing variant. There is no Idempotency-Key header contract. The saved first customer email and all attachments are used. You cannot supply a recipient, arbitrary content or URL. Limits are 100 attachments totalling 15 MiB, 24 MiB for MIME and an additional application limit of 4 MiB on the Outlook base64 body. Excess size fails before provider creation; no attachments are silently omitted.

HTTP 200 with `aangemaakt` proves only that a draft was saved. HTTP 202 with `onzeker` or `gestopt` still requires recovery. After losing a response, reuse the same request ID, provider, basis and language. Do not create a second request after an uncertain result: a new ID does not bypass the block. Use the fixed drafts-folder link to inspect the mailbox yourself. Entering the recipient and sending remain separate, deliberate mailbox actions. `verzending` stays `niet_aangevraagd`; this is not delivery evidence.

POST `/v1/gesprekken/{id}/mail/concepten/stoppen` accepts only `{aanvraag_id,bevestigd:true}` and returns HTTP 200 with the current receipt. Stopping remains available after trial expiry, while new creation does not. It prevents a new provider call at the final check, but does not remove drafts or cancel a provider request already in flight. A later proven provider response may still result in `aangemaakt`. Import examples leave request IDs, basis and keys empty and block mutations. They have not been executed.

## Save language preferences and read an existing letter

GET `/v1/rapporten/{id}/taalbeleid` or `/v1/gesprekken/{id}/taalbeleid` returns the current `basis`, `policyVersion` and `requestedSecondary`. Reports have `report` and `coverLetter`; conversations have `conversationReport`, `customerEmail` and `followUpEmail`. Missing saved choices explicitly default to `off`. Choose `off`, deliberately choose `inherit`, or use `specific` with a lowercase language code of two or three letters.

PATCH the same route with `{secondary,basis,policyVersion,bevestigd:true}`, both `voorkeuren:wijzigen` and `resultaten:lezen`, valid trial/package access and an Idempotency-Key of 8–128 safe characters. Send only the outcomes to change; other choices remain unchanged. Use the basis and version returned by the latest read. Version 0 in the collection is suitable only when that read returns 0. Saving starts no provider: `productie_gestart:false` and `start_productie_afzonderlijk:true` are explicit. The primary source language does not change.

For an exact replay, `ontvangenPolicyVersion` identifies the original receipt version. Other policy fields describe the current state, which may have changed. `changed` appears only in a new receipt, not a replay. Read again after a genuine version conflict. Paid language start/retry and workflow controls are described in the following sections; saving preferences never starts them implicitly.

The existing GET `/v1/taalvarianten/{opdrachtId}/coverLetter` now returns actual letter text through the fixed native reference. It allows at most 1 MiB of valid UTF8 and rechecks current source and target report access, variant, task and key. The target must be ready, belong to the same dossier and organisation, and be translated from the correct source into the task language. An unsafe reference or oversized, empty or unreadable letter does not produce partial content. Internal storage paths remain hidden. Only documentation files have been built; no requests have been executed.

## Deliberately start and track paid translations

Source `cfe078af` describes the new bound request chain around the existing native language producer. The older paid route already existed; this documentation does not claim that it was absent. There is no testing or publication evidence.

Read GET `/v1/gesprekken/{id}/taalproductie` or `/v1/rapporten/{id}/taalproductie`. Select only available outcomes and a concrete language different from the source. POST `/taalvarianten` beneath the same source accepts `{language,outcomes,basis,bevestigd:true}`, the current basis and a stable Idempotency-Key of 8–128 safe characters. Current processing/read permissions, language policy, trial/package access and personal actual-cost mode are required. Report translation may process both report and letter even when you request one outcome; inspect `verwerkt_uitkomsten` for that scope. Provider costs and customer credits are separate from quotation amounts and are not presented as zero or a fixed tariff.

Start returns HTTP 202 with `opdracht_id`, `versie`, `basis`, `status`, `herhaald` and `statusUrl`. Read that status route next. Reuse the exact body and key after losing a response; another key cannot adopt an existing native task or target translation. Status separates processing (`status`) from controls (`besturing`), current source changes and recovery flags. Read content through the existing language variant GET. Reading source context, saving preferences, starting a translation and reading content are separate actions.

POST `/v1/taalvarianten/{opdrachtId}/retry` uses the current task basis, task version, `bevestigd:true` and its own stable key. Recovery is allowed only before provider start with no target report, or when every requested result is saved and settled. In the latter case, new provider attempts remain blocked. Partial or uncertain output never implies permission for another paid attempt. A durable provider-attempt marker may require recovery even after a budget refusal.

## Workflow language production under 376-v2: tested locally and in CI

Source `a2f438b8` replaces the unconditional workflow block with complete `taalouder-v2` schema evidence. Start, restart and resume still return409 `taalproductie_ouderretentie_niet_vrijgegeven` when evidence is missing, old, partly closed or unreadable, before receipt, queue or provider. Activation, current original key/actor/run/source permissions, plan/trial and actual-cost mode remain required. This is not a live availability claim. Bulk secondary languages remain blocked.

GET `/v1/workflowruns/{runId}/taalproductie` returns `mogelijk:true`, `blokkade:null`, combined `basis`, `bron` and `opdrachten_meer` for a supported canonical completed report. The incomplete/unsupported shape keeps `basis:null` and no source. Context does not authorise a start. Start uses context.basis and run.versie; task controls use the task basis and version. The three synthetic examples are included and remain blocked by default.

`startblokkade` is null for direct or active workflow bindings, the retention code for bulk, and `taalproductie_ouder_vervallen` for archived workflow origin. Existing202 expiry transfers only a complete terminal and financially settled parent chain to source-bound evidence. Replay never recreates a removed run. Existing260/261 customer confirmation, holds, platform-job retention and separate storage completion remain required; this is no general account-purge claim. The [technical candidate guide](rapportly-crm-workflowtaal-kandidaat.md) describes the exact routes and retention boundaries in Dutch.

## Mailbox schema unavailable

Selective source `25aa00db` rechecks the required schema before each draft status read, stop request and creation request. Positive results are not cached. Incomplete or changed schema components, a read failure or deadline return HTTP 503 `mail_concept_schema_onbeschikbaar`, including for status reads and stopping. No new domain or provider start follows. A provider response already received can still be recorded through the existing completion path. The internal schema check has no public API route and proves neither tenant purge operation nor release or production activation.

## Save action points and followup

Read GET `/v1/gesprekken/{id}/followup`. It contains the current followup basis, visible action points with their own `basis`, draft email, customer and separate followup outbox. Points follow display order. A conversation that is not ready returns HTTP 200 with `followup:null` and no points. Read failures in this full source return HTTP 500; automation settings may separately fall back to safe draft defaults.

PATCH `/v1/actiepunten/{id}` requires `basis` and at least one of `status`, `tekst` or `deadline`. Status is `open`, `afgerond` or `vervallen`. Text allows at most 500 Unicode code points without silent truncation. A deadline is a real date in years 1000–9999 or null. PUT `/v1/gesprekken/{id}/followup` saves email with its current basis, up to 100 point updates with individual bases, or both. Point IDs must be unique. Email allows 20,000 Unicode code points; NUL and whitespace-only input are rejected. Use `actiepunten:beheren` and a stable Idempotency-Key of 8–128 safe characters.

The entire mutation is atomic. Reuse the exact body and key after losing a response. `ontvangst` remains the historical receipt, while top-level `basis` and `actiepunten` show current rows sorted by ID. Other edits may already have changed those current values. A new request with a stale basis conflicts and requires another read. Saving changes neither owner nor order, starts no AI and does not approve email. Sending remains a separate review action.

## Customer replies and timeline

GET `/v1/klanten/{id}/tijdlijn` combines at most 200 outgoing and 200 incoming rows, then filters by access and sorts by descending time. There is no cursor or guarantee of complete history. Disabled replies or a missing settings/replies table can return HTTP 200 with `beschikbaar:false`. That does not prove that no replies exist. Other read failures return HTTP 500. Incoming text and attachments remain data; sender status authorises no automatic command or response.

PATCH `/v1/klantreacties/{reactieId}/gelezen` accepts an empty or absent body and requires `klanten:beheren`. It needs no Idempotency-Key: repeated requests preserve the first `gelezen_op`. GET `/v1/klantreacties/{reactieId}/bijlagen/{index}` uses the zero-based index from the timeline and returns a download link valid for 300 seconds. Open it separately without an API key. The native handler checks object access and the fixed storage prefix before signing; this source has no additional access read after signing. No API requests or downloads have been executed in this build phase.

## Prepare a conversation from documents

Source `cfe078af` defines POST `/v1/gesprekken/documenten`. Attach 1–50 local synthetic files as repeated multipart parts named `bestanden`, each at most 50 MiB. Send `X-Rapportly-Client-Version` and one stable `Idempotency-Key` containing 8–128 safe characters. The collection contains no local file paths or real data and blocks mutations by default. Text fields are `transcript`, `titel`, `tier`, `klantId`, `taal`, `tweedeTaal`, `languagePolicy`, `generatieOpties` and `stijlenV2`; the last three contain JSON encoded as text. The normalised input together may contain at most 180000 UTF-8 bytes of JSON. Canonical input is described; all legacy normalisation variants remain open.

HTTP 202 returns `aanvraagId`, `status` and `statusUrl`, including an identical replay. This confirms receipt of preparation only. Preserve the exact request key, file order, names, MIME values and bytes on replay. Different input under the same key conflicts. After losing a response, read GET `/v1/gesprekken/documenten/jobs` with the same original request key, API key and client version. This returns one receipt, not a list. Use the received UUID with GET `/v1/gesprekken/documenten/jobs/{aanvraagId}`. That handler requires the original actor and organisation and current permissions; it adds no original API-key comparison. Both read routes check current customer access where applicable. Receipts become unavailable after seven days.

Statuses are `uploaden`, `ontvangen`, `start_bezig`, `klaar`, `mislukt` and `opruimen`. A failed preparation includes an error code. **Here, `klaar` means the conversation bundle has been started.** Then use `gesprek_id` to read the existing conversation status; do not upload again while waiting for the final result. The worker checks each source and rejects preparation if any file has no readable text. Accepting a presentation or image extension therefore does not prove extraction success. Combined text must contain at least 100 JavaScript code units. Current permissions, trial/package access, credits, personal actual-cost mode and durable processing remain required. No fixed price, completed cleanup, tested journey or live availability is claimed.

## Read and edit existing document drafts

Source `cfe078af` defines five routes under `/v1/rapporten/{versieId}/documentconcept`. Read metadata first. `beschikbaar:false` is a valid response when no draft exists; a storage failure is not converted into empty success. GET `/inhoud` returns the saved Markdown, readable text, any validated structure and the offered editable fields. Retain the exact `etag`, including double quotation marks. A draft is a derived revision; the original report and previous content remain available in storage.

PUT `/inhoud` accepts only `{markdown}` or `{wijzigingen:[{sleutel,waarde}]}`. For structured edits, use 1–2000 unique keys from the latest offered fields and preserve each value's type. This cannot remove fixed labels, table relations, source references or required functions. `bewerkwijze:structuur` requires those fields; replacing it with Markdown may return HTTP 422. The complete public JSON request is limited to 100 KiB. The internal 2 MiB content limit does not increase API upload capacity. Send the client version, a new stable request key of 8–128 safe characters and the current `If-Match`. Editing does not approve missing source checks or start formatting.

After losing a response, repeat the same request. The fast replay applies only while the current row still has that request key and exactly the same content; it is not a general promise for older requests after later edits. After a genuine version conflict, read again and review the current content.

POST `/opmaak` with an empty body explicitly requests formatting or supported content recovery. This also requires current permissions, valid trial/package access, client version, request key and `If-Match`. HTTP 202 means the formatting request was accepted. Continue reading metadata: `inhoud_beschikbaar`, `opmaak_bezig`, `gedeeltelijk` and `concept_klaar` are distinct states. `ontbrekendeVereisten` may identify a missing original layout or editable text. Readable text does not prove complete output.

GET `/download/pdf` or `/download/docx` returns a private link valid for 120 seconds, with the current content ID, revision and filename. Word also requires the package's current Word-download feature. The service checks the saved content binding and exact private path. Optional `bestandsnaam` is only a sanitised download label. The handler does not reread permissions after creating the link. Examples are synthetic, mutations are disabled by default, and no journey test or publication has taken place.

## Recovery overview and image library

GET `/v1/documentherstel/overzicht` requires `gebruik:lezen` and the current organisation administrator role. It returns technical counts for the fixed past 30 days, using each report version's highest draft revision updated within that period. The six status counts add up to `totaal`; one report can have several causes. No names, report IDs or content are included. An unreadable or invalid source result gives HTTP 503 rather than a zero overview. Reading it starts no recovery action.

GET `/v1/beeldbank` returns up to 500 visible images, newest first: team images and your own personal images. It includes the current deletion policy, available actions and use by active styles. Thumbnail links last 120 seconds; failed signing can produce `preview_url:null`. This is not a complete history of image use.

POST `/v1/beeldbank` accepts one local PNG or JPEG as `bestand`, with optional `naam` and `deling_scope`. New uploads are limited to 5 MiB and 25 million pixels, with at most 20000 pixels per side. The service stores a normalised PNG and thumbnail without original metadata. Choose `persoonlijk` deliberately; omitting the scope means `team`. HTTP 201 means a new row, while 200 reuses visible existing content. An existing team image with the same hash can also satisfy a personal request, so inspect the returned `deling_scope`. Upload does not retrieve usage information; `gebruikt_in` is empty in that response. Read the list for current use.

PATCH `/v1/beeldbank/{id}` changes the name and/or sharing scope. Only the owner can actually change sharing; another authorised member can rename a visible team image. PUT `/instellingen` with `team_verwijderen_alleen_beheer` requires an administrator and controls who can delete team images. DELETE `/v1/beeldbank/{id}` returns HTTP 204 without content, including a valid already missing or invisible row. The deletion marker is authoritative; byte and quota cleanup follows on a best-effort basis. This does not prove physical purge, and frozen style versions retain their own copies.

These image-library mutations require `stijlen:beheren`, current trial/package access and a shared rate limit of 30 per minute per IP. These routes have no Idempotency-Key, ETag or client-version contract. The import example leaves the file choice empty and explicitly requests a personal synthetic image; mutations remain disabled by default. The source contract is written, with no tests or publication performed.

## Replay responses for paid revisions

Source `cfe078af` defines POST `/v1/uitvoer/rapport/{ankerId}/rapport_markdown/hergenereer` and the same route pattern for conversation fields `verslag`, `interne_mail`, `klantmail` and `followup_mail`. Send the current positive `basis_versie`, a trimmed instruction of 10–2000 JavaScript code units and a stable request key of 8–128 safe characters. For a genuinely new request, use a new key after reviewing the content. Version 1 in the collection is only a placeholder; replace it with the version actually read. Paid requests remain disabled by default in the collection.

For a report, HTTP 202 returns `mutatie_id`, `taak_id`, `status:bezig` and `creditmodus:persoonlijke_api_kosten`. Retain the mutation ID. A replay that has completed may return HTTP 200 with only `ok`, `versie_nummer`, `tier` and `creditmodus`; the IDs are not repeated. A saved terminal failure returns its stored HTTP status, such as 402, 409, 429 or 502. These responses contain neither report text nor a current balance.

A conversation field still returns HTTP 202 after a completed replay. Inspect `status`: `bezig`, `klaar`, `mislukt` or `controle_nodig`, alongside `fase:ontvangen|schrijven|opslaan`. A completed result may include its saved version number; a failed result may include `code`. Revising email text does not send a message.

GET `/v1/uitvoermutaties/{id}` requires the original API key, actor and organisation and current object permissions. It uses technical statuses `queued`, `processing`, `succeeded`, `failed` and `reconcile_required`. This distinction matters for reports: the start response may still say `bezig` when recovery checks are needed. Never bypass an uncertain result with a new paid request key. After success, read current output text and history. Saved fields from older internal credit accounting are not a price promise; use the usage endpoints for actual costs. No test or publication has been performed.

## What an initial generation receipt proves

Current source `cfe078af` returns **HTTP 200 with a durable receipt** for POST `/v1/rapporten`, `/v1/offertes`, `/v1/presentaties` and `/v1/gesprekken`. This does not mean content is complete. Documents return `versie_id` and `versie_nummer`; conversations return `gesprek_id`. The current receipt contains no `job_id` or terminal `status`. Use the existing source-status and download routes to follow processing.

The receipt includes the primary language, requested language policy, resolved outcome languages, policy version, tier and length band. New full API starts use `creditmodus:persoonlijke_api_kosten` and `credits_schatting:null`; this is not a zero price. A neutral document can return `voorbeeldprofiel:{id:null,versie_id:null}`. Both variant lists in this receipt are always empty, including on replay. Read the separate language status for actual translations. A possible `taalbeleidWaarschuwing` means document processing may continue while a secondary-language preference was not safely saved.

After losing a response, retain the same request key and input. A replay can reuse the same target; changes in source, style or language context may still conflict. Replaying a start does not restart a terminal failed worker. Document starts may return `Idempotency-Replayed:true`. This source makes no such header promise for conversations.

A structured quote may also return `offerte_snapshot:{id,sha256}`. An exactly completed quote claim can replay its saved response before new profile, catalogue or token checks; general public access checks still run first. If the quote has started but its snapshot or replay receipt is not yet safely saved, HTTP 503 may nevertheless include `versie_id`. Do not create a second quote with a new key. Historical quote claims store a 2xx status and JSON object without complete current DTO normalisation, so all older response variants remain explicitly open. The new schemas describe the current producer and have not been tested or published.

### What a frozen document style contains

A workflow can retain the selected profile version and its render identity. The shape differs by engine: a classic profile, a Word template, a PDF source style or a PowerPoint template. OpenAPI now describes their concrete hash and version fields and the Word/PowerPoint capabilities. Word capabilities include heading levels, tables, lists and an optional form projection. PowerPoint capabilities cover available layouts, text placeholders and table/chart limits. These limits describe what the frozen renderer can process; they do not confirm completed document production.

The current PowerPoint producer uses a limit of 200 slides and four chart series with twelve points. The source also accepts a specific older slide limit when the rest of the frozen identity matches exactly. The Word manifest fallback can return null Markdown capabilities; that does not establish a working render path. Historical nested example manifests and recognition records remain explicitly partial. Current profile permissions, the active version, the choice hash and native render checks still apply.

### CRM settings, visit status and writes

GET `/v1/klanten/crm-instellingen/{soort}` reads the current revision and up to30 stored JSON values. GET `/v1/klanten/{id}/crm-bezoek` retains the existing sinds/tot shape when writes are disabled; with CRM writes enabled it also includes revisie. Reading does not mark anything as read or approved.

The four write contracts for templates, saved searches, visit markers and trajectories originate from `d964db8c` and are available under the documented release conditions. The production test covers three write routes; templates were only read. They require the CRM gate, complete privacy375 readiness, current permissions, bevestigd:true and Idempotency-Key16–128. Settings replace the complete list; visit markers use the re-read server boundary and revision. Replay returns current state plus the original receipt without restoring old settings. Stale revisions or changed input under the same key return409. Offboarding blocks writes; the existing365-day policy does not prove that a general account purger is connected. The [technical candidate guide](rapportly-crm-workflowtaal-kandidaat.md) provides the exact contract and synthetic examples in Dutch. Collection mutations remain blocked by default.

### Following CRM pages without skipping results

`GET /v1/klanten/crm-werk` returns up to ten clients with up to 25 open action items each. Use `pagina.volgende` for the next client page. To read more action items for one client, send `klantId` and `actiepuntNa`, without the client cursor `na`. Ordering is by UUID, not deadline. Stored CRM data remains JSON inside a fixed envelope.

`GET /v1/klanten/{id}/crm-reacties` selects up to fifty candidates and then applies current source permissions. An empty list can therefore still have `pagina.meer:true`. Always use the server cursor, even when it does not identify a visible item. Pages are not a fixed snapshot and provide no total count. Historical office reactions can remain associated with revoked shares while the underlying document is currently readable and shareable; they do not reopen the client portal.

The existing `GET /v1/uitvoermutaties/{id}` now rejects all query parameters. The original key binding and current delegation are checked before and after the read; a report or conversation that is no longer visible returns 404. Read the specific error code and do not treat a failed read as evidence that an earlier mutation did not occur.

### Receiving and continuing one fixed bulk import

`POST /v1/bulkimports` receives one complete set of 1–400 rows with unique row numbers, your own `bulkImportId` and an active report profile. The general JSON limit remains 100 KiB even when individual row fields allow larger values. Groups contain up to forty numerically sorted rows, with ten minutes between their earliest scheduled start times. This does not guarantee completion time or a fixed credit price.

Retain the complete original body and request key. After a lost response or partial scheduling, repeat exactly that request, including row order; do not send a smaller remaining list or create a new import ID. Existing failed or uncertain jobs are not restarted by this replay. Current permissions, profile sources, package access, cost mode and schema must still be available.

Read `GET /v1/bulkimports/{id}` using your original `bulkImportId`. The response field `opdracht_id` is a different identity bound by the server. `hoofdrapport_klaar:true` proves only the primary report; import-level `klaar:true` requires every requested output for every row. A missing linked job can currently produce a null primary-report marker. Row status distinguishes missing language output, processing and required recovery. Historical language status remains visible, but this release blocks every new or repeated import with an effectively enabled second language. A replay cannot complete missing language output.

The existing Gratis limit is two top-level report starts per organisation per UTC day. A bulk row and its report child are not counted twice. `429 gratis_daglimiet` differs from an uncertain input/queue failure with 503. Only an explicit database rejection proves rollback of that enqueue. Do not bypass uncertainty with a new request key. An already rejected version is not automatically restarted after midnight. No new public day-cap status endpoint is described.

### Office portal: invitations, documents and revocation

First read `GET /v1/klanten/{id}/portaal` with `klanten:delen`. Retain the current `basis` and opaque `adres_basis`. Invitations use the address already stored in the client folder; the API accepts no recipient address, login link, OTP or redirect. Invite only a synthetic test client at a controlled test address. Import examples have blank identifiers and never run automatically.

`POST /v1/klanten/{id}/portaal` requires both basis values, `bevestigd:true` and a stable request key. An uncertain auth/mail response returns 202. A replay can return 200 while the receipt is still uncertain: always inspect `ontvangst.status` and `herstel_nodig`. `mail_aangeboden` is not delivery evidence. Never use a new key to bypass uncertainty. Stopping is a separate deliberate action on the original request; it does not delete an auth account or undo an external attempt already submitted. A stopped attempt can therefore still require recovery.

Sharing and revocation are separate from invitation. Sharing requires current permissions on a completed report with PDF or a supported existing dossier document. Recordings, audio and transcripts cannot be shared through this action. `DELETE /v1/klanten/{id}/portaal` disables access; DELETE on an individual share revokes only that document. Neither deletes the original document. Read the new basis after each new change; after response loss, repeat the exact original request. `huidig` is current, while `ontvangst.basis` may be historical.

The overview contains up to fifty recent submissions and currently visible shares; it does not paginate older submissions. A linked identity (`heeft_toegang`) differs from active access (`actief`). These six office actions do not expose the client JWT router. Separate portal release approval and all current permissions remain required.

### Recovery after complete translation, before publication

Selected source `5edf9824` privately retains a complete validated report/cover-letter translation with proven actual-cost settlement before rendering. The existing deliberate retry or resume action can render, store and publish the same content without a new provider call or reservation. The original task, target version, source basis, key, actor, parent and cost evidence must still match. This path cannot overwrite an active canonical report revision. Read the current status first and use the current task basis/version.

`herstel_zonder_provider` describes proven stored output or complete recovery content; it is not general permission to restart. A missing letter, partial content, unknown provider response or uncertain settlement keeps paid retry closed. New paid outcome selection from `28d23f2e` is excluded from this release guide and import collection. Conversation, presentation, form/quote and unbound legacy recovery receive no additional permission. There is no new public recovery-receipt route.


### Bulk import without a second language

Source `56179ec5` rejects an effectively enabled second language with HTTP 409 `api_bulkimport_taalretentie_niet_vrijgegeven` before receipt, private storage or scheduling. Omitted policy or `inherit` can activate an office default. For a new import, explicitly set both secondary outcomes to `off`, as the collection does. Replay retains the original frozen policy; do not change its body to bypass the boundary. New primary-language bulk remains subject to its existing contract. The boundary does not cancel primary report jobs already scheduled.
