# Integration recipes

These recipes translate the published OpenAPI 1.71.0 contract into implementable integration sequences. The OpenAPI document remains authoritative for every schema, enum, status and error code.

Use the server URL `https://api.tapinomahub.com` and append the paths shown below. Send `Accept: application/json` and keep `X-Api-Key` in a server-side secret store. A sandbox key uses the same server URL and follows the documented sandbox behaviour.

## Rules shared by every recipe

### Tenant and concurrency rules

- The API has no tenant field. A Tenant is separated by assigning it its own Client, normally a Sub-user or partner-workspace response, and using that Client's API key.
- Never select a tenant by request-body data, VIN, OE number or a caller-supplied header. Resolve the authenticated tenant to one stored API-key secret before constructing the request.
- The contract allows one active API request per Client. Serialize calls for the same Client. Different Clients may run in parallel.
- A `tapiId` is accessible only with the Client key used for the documented VIN flow. A foreign or unknown `tapiId` returns `404`.
- Never send an API key to browser code. The browser redirect flow returns a `redirectUrl`; the server-side API key is not part of that redirect.

### Error and retry rules

- Parse the HTTP status first and the stable top-level `error` value second. Do not branch on the human-readable `message`.
- Treat `400`, `404` and `422` as data or workflow decisions. Correct or reconcile the input before another attempt.
- Treat `401` and `403` as authentication, authorisation, Client-state or sandbox-availability decisions. Do not rotate or replace a key automatically.
- Treat `402` as a billing or coverage decision. Do not loop while the plan, balance or sponsorship state is unchanged.
- For `409`, branch on the documented `error`. In particular, never hide `vin_provider_mismatch`, `redirect_required` or an idempotency conflict behind a generic retry.
- For `429`, honour `Retry-After`. `client_request_in_progress` also means the Client queue is not correctly serialised.
- Retry `500`, `502` and `503` only when the corresponding error-catalogue entry is retryable, and use a bounded backoff. A write retry must reuse its original `Idempotency-Key`.
- The contract states that a pre-charged call ending at HTTP 400 or above is refunded automatically, subject to the explicitly documented combined-VIN exceptions. A refund does not turn a failed result into a success.
- Capture `X-Tapinoma-Billing-Source`, `X-Tapinoma-Billing-Bundle`, `X-Tapinoma-Sponsorship-Grant` and `X-Tapinoma-Usage-Warning` when present. Warning text is for people and is not stable program input.

### Idempotency rules

- Send `Idempotency-Key` only to operations that declare it and follow that operation's schema. Intelligence explicitly requires it on `GET /parts/intelligence`, `GET /parts/clusters/{clusterId}/intelligence` and `GET /parts/clusters/{clusterId}/history` as well as its VIN POST; neither Intelligence status poll declares it.
- Persist the key before the network call. The same Client, operation and canonical request body must reuse the same key after a timeout.
- A first stored success reports `X-Tapinoma-Idempotency-Stored: true`; a replay reports `X-Tapinoma-Idempotent-Replay: true`. Successful replay records are retained for 24 hours.
- On `409 idempotency_request_in_progress`, wait for `Retry-After` and repeat with the same key. On `409 idempotency_key_conflict`, stop because the key was used for different input. On `409 idempotency_result_unavailable`, do not substitute a new key automatically.
- Treat the preceding stored/in-progress/result-unavailable rules as operation-specific. The four initiating Intelligence pilot operations document only the replay header and `409 idempotency_key_conflict`; they do not declare the stored header, the other two central `409` states or a 24-hour retention promise.
- `createClientUser`, `createClientUserApiKey` and `createPartnerWorkspace` issue a raw API key and reject `Idempotency-Key`. After an uncertain response, do not repeat them blindly.

### Asynchronous rules

- Six documented job families have a polling contract: VIN parts, cart check, VIN economic evaluation, part-image generation, part-cluster refresh and VIN Economic Intelligence.
- A `202` is an accepted job, not a business result. Persist `jobId`, `statusUrl`, `createdAt`, the originating operation and the original business key before acknowledging the request locally.
- Honour both the `Retry-After` header and the `retryAfterSeconds` body value. If both are present and differ, wait for the longer interval and record the discrepancy. Use the required full `statusUrl`; compare `Location` and quarantine a mismatch rather than origin-resolving its current root-relative example. Do not resubmit the initiating operation merely because it returned `202`.
- Continue for `queued` and `running`. Older status resources can express those states inside HTTP `200`; the two Intelligence status operations return HTTP `202` while pending and HTTP `200` with the final result. Require the workflow's usable result before completion and quarantine invalid state/result combinations. A required `vdi` array that is present as `[]` is a complete VIN-parts field value, not a reason to continue polling.
- Polling belongs to the original order and does not create a second order; rate limits and the single-active-request rule still apply.

### Storage and retention

Operation-specific expiry and refresh behaviour does not grant a general right to retain request or response data.

> **Storage and retention:** The public contract does not grant a general right to retain submitted content or returned data. The integrating system must apply an approved retention policy, minimise personal data and copy an output asset only when that policy permits it.

## Recipe 1: ERP vehicle, parts and cart integration

### Goal and non-goals

**Goal:** enrich an ERP vehicle record, retrieve the correctly characterised parts set and optionally check one to 30 ERP cart positions against a VIN.

**Non-goals:** creating orders, changing stock, proving installation compatibility from a type-level or incomplete result, or treating tapinomahub content as the ERP's accounting record.

### Prerequisites

- One server-side API key per ERP tenant or end customer.
- A legal basis for transmitting the VIN and using the returned data.
- A configured numeric `provider` value permitted by the Client's contract.
- For browser-assisted route `1`, an HTTPS `returnUrl` without credentials or fragment and a server-side correlation store. The ERP should generate an opaque `state` and enforce one-time use locally; the API contract only promises to return the value unchanged. Any continuation to VIN-dependent operations additionally requires a full 17-character VIN known independently of the callback.
- An ERP business key for the vehicle and, for cart checks, a stable request key and an ordered list of at most 30 OE positions.

### System-of-record boundary

The ERP remains authoritative for customers, the individual physical vehicle, work orders, cart lines, quantities, stock, negotiated prices and accounting. The tapinomahub API supplies derived vehicle, parts and comparison data. Subject to the approved data policy, keep the VIN and ERP vehicle ID distinct from `tapiId`: `tapiId` identifies a technical vehicle type and several VINs may share it.

### Operation sequence

1. Optionally verify connectivity with `GET /ping` (`getSystemStatus`). Do not send `Idempotency-Key` to this read operation.
2. Establish vehicle context with exactly one of these entry branches:
   - **Direct server-to-server branch:** call `GET /vin/{vin}/vehicle` (`getVehicleByVin`) with explicit `provider=2` or `provider=3`, the two-letter `country`, and explicit choices for the optional `includeEquipments`, `includeColors` and `includeTechnical` flags, or deliberately accept their `true` defaults. Carry forward the returned `vin`, `tapiId`, `provider` and `incomplete` values.
   - **Browser-assisted branch:** server-side, call `POST /vin/redirect-sessions` (`createVinRedirectSession`) with a 3–17-character `vin` input, HTTPS `returnUrl` and an opaque `state`. Use an `Idempotency-Key`. Redirect only the user agent to the returned `redirectUrl` before `expiresAt`. On the callback, require the original `state` and, as a local anti-forgery control, reject a state already consumed by the ERP; accept `tapiId` only when `status=completed`. A cancellation has no `tapiId`. The callback does not contain the VIN completed in the browser. Fetch technical type data with `GET /vehicles/{tapiId}` (`getVehicleByTapiId`), but treat an original 3–16-character input as a terminal boundary for later VIN-dependent calls until a full VIN is obtained independently.
3. Only with a known full 17-character VIN, retrieve parts through route `1` with `GET /vin/{vin}/parts` (`getPartsByVin`). Omit the optional `provider` field unless the integration explicitly needs it; if it is sent, use the documented value consistently.
   - On `200`, consume `vin`, `tapiId`, `provider`, `matchLevel`, `allCategorySigns`, `missingCategories` and `parts`; every part contains required `tapiGenArt` and `vdi` fields, and `vdi=[]` is valid when no mapping is available.
   - On `202`, persist `jobId`, `statusUrl`, `vin`, `provider`, `createdAt` and retry timing, then poll `GET /vin/parts/jobs/{jobId}` (`getVinPartsJob`).
4. If the ERP needs cart verification, call `POST /vin/cart-check` (`checkVinCart`) with `{vin, country?, mode, oeNumbers}` and an `Idempotency-Key`. `mode` is exactly `type` or `vehicle`; preserve request order and duplicates.
   - On `200`, map `results` back to cart lines by array position and keep `complete` with every result.
   - On `202`, persist the returned cart-check `jobId` and `statusUrl`, then poll only `GET /vin/cart-check/jobs/{jobId}` (`getVinCartCheckJob`).
5. Optionally read monthly usage with `GET /client/usage` (`getClientUsage`) using the same tenant key. This is monitoring data, not an ERP financial posting.

### Identifiers carried forward

| Identifier | Source | Destination and rule |
|---|---|---|
| ERP vehicle ID | ERP | Local correlation only; never sent as a substitute for VIN or `tapiId`. |
| Full `vin` | ERP input or an independently confirmed response | Vehicle, parts and cart operations. A browser callback returns no completed VIN, so its original partial input cannot be promoted to this field. |
| `state` | ERP | Redirect callback correlation echoed unchanged by the API; the ERP verifies it and enforces one-time use locally before accepting `tapiId`. |
| `tapiId` | Vehicle flow or redirect callback | `getVehicleByTapiId` and later vehicle-listing calls; store as a type-level reference. |
| `provider` | Configured request and tapinomahub response | Keep the returned numeric value when the approved policy permits it and use only values documented for the selected operation. |
| `jobId` / `statusUrl` | `202` response | Only the matching documented polling operation. Do not treat it as an ERP job ID. |
| OE position index | ERP cart | Response `results` preserves order and duplicates; map by position, not by deduplicated OE number. |

### Validation, errors, idempotency and tenant handling

- Reject an empty or unauthorised VIN before the API call. Do not infer missing VIN characters. Permit the documented 3–16-character input only for redirect-session creation; require an independently confirmed full 17-character VIN before parts, cart, economic or recall continuation.
- Before continuing, require the business identifiers used by the next step. In particular, quarantine a successful direct-vehicle payload that lacks `vin`, `tapiId` or `provider`; the current `VinVehicleResponse` schema does not mark these properties as required.
- Likewise, validate a `VinPartsResponse` before use. Quarantine a successful payload that lacks `vin`, `provider`, `matchLevel`, `allCategorySigns`, `missingCategories` or `parts`; the current schema describes these top-level properties but marks none of them as required. `tapiId` is explicitly nullable. Every returned part must contain a valid `tapiGenArt` and a `vdi` array; preserve an empty array instead of treating the position or job as incomplete.
- A direct vehicle request with `provider=1` can return `409 redirect_required`; start the documented redirect flow instead of retrying the same direct request.
- On `409 vin_provider_mismatch`, use the numeric route documented for the operation. Never silently switch the route.
- `incomplete=true` means the vehicle response is not an exact single-vehicle resolution. `matchLevel` must remain attached to the parts data.
- A non-empty `missingCategories` list means the parts list is incomplete. In a cart response, `fits=false` combined with `complete=false` is not a definitive exclusion.
- Use idempotency for redirect-session creation and cart checks. The VIN and parts reads are GETs and must not receive the header.
- Queue all initiating calls and their polls per Client. Use the same Client key throughout the documented VIN flow.

### Persistence fields

Subject to an approved data-class policy, model the following logical fields: local tenant ID, ERP vehicle ID, API Client ID or secret reference, VIN input and confirmed VIN, `tapiId`, returned `provider`, `incomplete`, `matchLevel`, category completeness, approved parts result fields including required per-position `tapiGenArt` and `vdi`, cart request positions, cart `complete` and results, originating `operationId`, OpenAPI version `1.71.0`, request hash, idempotency key where applicable, HTTP status, job fields, response timestamps, billing headers and structured error status/code. This list defines integration state, not permission to retain a complete response. Keep only the minimum permitted subset for the approved duration, and never persist the raw API key in business tables.

### Acceptance tests

1. A documented sandbox VIN completes the direct branch and stores distinct VIN and `tapiId` fields.
2. The redirect branch rejects a callback with wrong or reused `state`, accepts `status=completed`, and handles `status=cancelled` without creating a vehicle mapping.
3. A `200` parts result preserves `provider`, `matchLevel`, category completeness, quantities, every `tapiGenArt` and every required `vdi` array, including `[]`.
4. A sandbox `202` parts job survives a process restart and reaches `succeeded` without resubmitting the initiating call.
5. A unit test built from the documented production `failed` job example treats nested `error.code` as terminal even though polling returned HTTP `200`.
6. A completed redirect that began with a partial VIN can use its `tapiId` routes but cannot enter parts or cart calls because the callback supplies no completed VIN.
7. A schema-valid but incomplete VIN-parts payload enters quarantine instead of creating an ERP parts list.
8. Duplicate OE cart positions remain distinct and in order.
9. `fits=false` plus `complete=false` is rendered as unresolved, not incompatible.
10. Repeating a cart check with the same idempotency key produces no duplicate local operation and accepts `X-Tapinoma-Idempotent-Replay: true`.
11. Calls for two Clients can run concurrently, while two calls for one Client are serialised.

### Operational boundary

The ERP must remain the recovery authority for policy-approved data and must not use API-side persistence as its recovery store. Retain data only under an approved policy.

## Recipe 2: DMS document extraction

### Goal and non-goals

**Goal:** extract a stable international vehicle-registration structure, rich source-backed vehicle/equipment data or the documented generic automotive document structure and attach the derivative result to a DMS record.

**Non-goals:** replacing the original document, proving authenticity, making OCR output legally authoritative, or using a generic document response as a vehicle-registration standard.

### Prerequisites

- A DMS document ID, immutable source version and checksum.
- A publicly retrievable `fileUrl` that the caller is authorised to transmit and process.
- A per-tenant API key and a data-minimisation decision for holder data.
- `quality=standard` for personal-document operations; no other processing level is currently offered by the contract.
- Parsers generated from the pinned OpenAPI schemas `RegistrationDocumentV2Response`, `DocumentCalculationResponse`, the deprecated 11-field `DocumentExtractionResponse` and, when rich vehicle/equipment extraction is required, `VehicleDocumentExtractionResponse`.

### System-of-record boundary

The DMS owns the original bytes, access control, legal hold, source checksum, document version and deletion policy. The tapinomahub response is a derived extraction. Human corrections belong in a separate DMS layer; never overwrite the original document, and retain an unmodified response only when the approved policy explicitly permits it.

### Operation sequence

1. For an international vehicle registration document, call `POST /scanner/document/registration/international` (`extractInternationalRegistrationDocument`) with `{fileUrl, quality?}` and an `Idempotency-Key`. New integrations should use `fileUrl`, not request-property or path aliases. Use `POST /scanner/document/registration` (`extractRegistrationDocument`) only when the national `{status,data}` response contract is explicitly required.
2. Require HTTP `200` and `format="tapinoma.vehicle-registration.v2"`. Persist the `document`, `registration`, `holder`, `inspection`, `vehicle`, `fields` and `warnings` blocks as one versioned result.
3. Index `fields[]` using the canonical `code`. Keep `sourceCode`, canonical English `name`, normalized `value` and `sourceValue` together. Do not replace proper names, addresses or identifiers with translated guesses.
4. For a repair cost calculation, appraisal, invoice or similar calculation document, call `POST /scanner/document/calculation` (`extractCalculationDocument`) with `{fileUrl, quality?}` and a separate `Idempotency-Key`. Its fixed `DocumentCalculationResponse` has 12 required top-level fields: `documentType`, `documentSubType`, `source`, `document`, `vehicle`, `financials`, `repairAssessment`, `parts`, `labor`, `paint`, `notes` and `equipment`. There is no request field for a caller-defined output schema. Preserve `equipment[].codes[].value` as a string together with `equipmentKind`, `sourceSection` and `sourcePage`.
5. For a vehicle order or configuration where detailed identifiers, powertrain, equipment, warnings or the explicit sensitive-data boundary are needed, call `POST /scanner/document/vehicle` (`extractVehicleDocument`) with `{fileUrl, quality?, includeSensitiveData?}`. Keep `includeSensitiveData=false` unless the caller has an approved, purpose-bound need for the omitted personal, bank, payment or contract fields. Validate `schemaVersion="1.0"` and persist only the approved subset of `document`, `vehicle`, `equipment`, `sensitiveData` and `warnings`.
6. The four old paths are deprecated compatibility aliases and never HTTP redirects: `/scanner/document/extract`, `/scanner/vehicle-document/extract`, `/scanner/registration-document` and `/scanner/registration-document/v2`. Follow the documented response and idempotency behaviour for each path.
7. If the input is a photograph whose sole purpose is VIN reading, call `POST /scanner/vin/extract` (`extractVinFromImage`) with `{imageUrl, quality?}`. Continue only when the returned `vin` is non-null and has all 17 characters.
8. If DMS workflow explicitly requires vehicle enrichment, pass the confirmed VIN into the ERP recipe's documented VIN entry branch. Keep the extraction result and vehicle lookup result as separate records.

### Identifiers carried forward

| Identifier | Source | Destination and rule |
|---|---|---|
| DMS document ID and version | DMS | Local correlation and idempotency-key derivation only. |
| Source checksum | DMS | Detect source changes; a changed document is a new business operation and needs a new idempotency key. |
| `fields[].code` | Registration response | Canonical field mapping key; retain `sourceCode` and `sourceValue` for traceability. |
| Calculation `equipment[].codes[]` | Calculation response | Keep code system, original string `value`, nullable `normalizedValue`, `equipmentKind`, `sourceSection` and `sourcePage` together; never coerce leading-zero codes to numbers. |
| `equipment[].codes[]` | Rich vehicle-document response | Keep code system, original string `value` and nullable `normalizedValue` together; never coerce leading-zero codes to numbers. |
| `vin` | `vehicle` block or VIN-image response | Optional input to a separate VIN workflow only after format and completeness validation. |

### Validation, errors, idempotency and tenant handling

- Ensure the source URL is an authorised absolute URL and keep it available until the synchronous response completes. The contract does not promise that inaccessible private storage can be fetched.
- Validate the whole response against the named schema before indexing any field. Null and empty arrays are valid documented outcomes; do not manufacture missing values.
- Treat `documentType` and `documentSubType` from `extractCalculationDocument` or the deprecated `extractDocument` path as uncontrolled strings, not enums.
- In `extractVehicleDocument`, validate `equipmentKind` and `availability` independently. Preserve `standard`, `variant` and `special` as the documented kind axis and do not derive availability or pricing from it.
- A null VIN is a successful "not reliably readable" outcome, not a transport failure.
- Use a different idempotency key for the registration, rich vehicle-document, calculation and VIN-image business operations, even when they refer to the same source file. Reuse a key only for an identical retry. The canonical and deprecated paths for national registration, international registration and rich vehicle extraction share idempotency because their responses are identical. Never move one key between canonical calculation and deprecated document extraction: the latter omits `equipment`, so cross-contract reuse fails with HTTP `409`. Do not send an idempotency key when `extractVehicleDocument.includeSensitiveData=true`; the endpoint rejects it and never stores that sensitive response for replay.
- Route the request with the source document's tenant key. Never select the key from data extracted from the document.
- Apply the shared error policy. In particular, do not retry an unfetchable URL indefinitely; first restore authorised reachability or issue a new URL.

### Persistence fields

Subject to an approved data-class policy, model the following logical fields: tenant ID, DMS document ID and version, source checksum, a policy-permitted source reference, operation ID, OpenAPI version, quality, sensitive-data opt-in state, request hash, idempotency key where permitted, response `format` or `schemaVersion` where present, only the response fields approved by the mapping, source-preserving vehicle/equipment code strings, warnings, extraction timestamp, validation status, human-review status, HTTP status, billing headers and structured error. This list is not permission to retain the raw response or source document. Minimise and field-restrict holder and other personal data; block production persistence until purpose, duration, deletion and access rules are approved.

### Acceptance tests

1. A sandbox international-registration call validates against its named OpenAPI response and maps every `fields[]` element without losing `sourceCode` or `sourceValue`.
2. A missing optional value remains null; the adapter does not populate it from another field.
3. Calculation extraction maps empty `parts`, `labor`, `paint` and `notes` arrays without converting them to null and always returns the twelfth top-level `equipment` field.
4. A multi-page calculation example preserves every documented equipment entry, keeps `codes[].value` as a string including leading zeroes, and does not mix entries from another section into `equipment`.
5. Rich vehicle-document extraction keeps leading-zero equipment codes as strings and never infers `availability` from `equipmentKind`.
6. Vehicle-document extraction with `includeSensitiveData=true` sends no idempotency key and persists no replayable raw response.
7. Replaying one key and body through either path of the national-registration, international-registration or rich-vehicle alias pair produces the same result without a second execution or charge.
8. Reusing a legacy document-extraction key on canonical calculation extraction, or the reverse, returns HTTP `409`; the adapter neither accepts an 11/12-field cross-contract replay nor invents a replacement key.
9. A VIN-image result with `vin=null` creates a review outcome and does not start a vehicle lookup.
10. A source-version change creates a new idempotency key; a network retry of the same version reuses the previous key where the operation permits it.
11. The same document ID in two tenants produces isolated calls with two different Client keys.
12. Logs and support payloads exclude the API key and unnecessary document content.

### Operational boundary

Apply the DMS's approved retention and deletion policy to source files, extracted holder data and scanner results. Do not infer a retention period from the API response.

## Recipe 3: marketplace or shop catalogue enrichment

### Goal and non-goals

**Goal:** turn a confirmed OE number into traceable part master data, indicative pricing, a publication-ready marketplace article — eBay listing title, category, item specifics, keywords and shop SEO text — and optional synthetic publication images; optionally generate text for a vehicle already identified by `tapiId`.

**Non-goals:** automatically publishing unchecked content, setting the seller's final price, proving vehicle fitment from references, or presenting a generated image as a photograph of the stock item.

### Prerequisites

- A shop item ID and the original OE-number input.
- A configured manufacturer hint where available, target language, `vehicleType`, currency and a contract-supported `marketplaceId` enum value. The documented marketplaces are the eBay sites `EBAY_AT`, `EBAY_AU`, `EBAY_BE`, `EBAY_CA`, `EBAY_CH`, `EBAY_DE`, `EBAY_ES`, `EBAY_FR`, `EBAY_GB`, `EBAY_HK`, `EBAY_IE`, `EBAY_IT`, `EBAY_NL`, `EBAY_PL`, `EBAY_SG` and `EBAY_US`, with `EBAY_DE` as the default. SEO languages are `de`, `en`, `fr`, `es`, `it`, `nl`, `pl` and `zh`; do not invent enum values.
- Publication rules for minimum content quality, live availability, synthetic disclosure and human review.
- Authorised reference-image URLs if image generation uses them.

### System-of-record boundary

The shop owns SKU identity, stock, tax, final price, offer state, publication approval, customer orders and its stored media. The tapinomahub API supplies normalized references, documented type-level fitment, descriptive enrichment, an indicative price evaluation and generated assets. `fitment`, `references`, `referenceNumbers`, `replacementChain`, aftermarket references, VDI codes and generated visuals remain evidence or enrichment, not a stock identity or installation guarantee.

### Operation sequence

1. Call `GET /parts/oe/normalize?oeNumber={raw}&manufacturer={optional}` (`normalizeOeNumber`). Continue automatically only when `status=matched` and `normalizedOeNumber` is non-null. Persist `version`, `matchRule`, `confidence`, replacement-family metadata and candidates even when the outcome is not matched.
2. Call `GET /parts/oe/{oeNumber}` (`getOePart`) with the confirmed `normalizedOeNumber`. Carry forward response `normalizedOeNumber`, `tapiGenArt`, `vdi`, `part`, `fitment`, `replacementChain`, `references` and `referenceNumbers`. Preserve `part.name` exactly as returned, including `null`. Treat `tapiGenArt` and `vdi` as independent best-effort enrichments: accept `null` and `[]` without discarding the confirmed base part or the other classification. Keep `vehicleTypeKey` opaque and preserve every fitment entry's criteria with it. `fitment` contains the available type-level assignments and can be empty. Preserve each `{manufacturer, numbers}` reference group and the exact closed `referenceNumbers.oe_oem_reference_numbers[]` list. Read relationship direction only from `replacementChain`; do not derive either reference view from that chain or the chain from those views.
3. For an indicative market range, call `GET /parts/oe/{oeNumber}/price` (`getOePrice`) with explicit `condition`, ISO 4217 `currency` and `vehicleType`. Persist the returned parameters, `result`, `new`, `used` and `priceRecommendation` together.
4. For the marketplace article itself, call `GET /parts/oe/{oeNumber}/seo` (`getPartSeo`) with the configured `marketplaceId`, `language` and `vehicleType`, and map the response onto the listing form: `content.ebayTitle` onto the eBay listing headline within its 80-character limit, `categoryId` onto the marketplace category of the requested `marketplaceId`, `itemSpecifics[]` onto the article's item-specific fields as `{name, value[]}`, `keywords[]` onto search terms, `product` onto product naming, and `content.title`, `content.h1`, `content.metaTitle`, `content.metaDescription`, `content.slug` and `content.bulletPoints` onto the shop page. Persist the returned publication fields with the generated text. `404 seo_no_exact_match` is a documented no-match for that OE number and marketplace, not a failure of the item flow.
5. If a standalone part designation needs translation, call `GET /translation/translations?sourceLanguage={language}&text={one-part-designation}` (`translatePartName`). The `text` parameter must contain exactly one part designation, not a title, sentence, list or description.
6. For synthetic publication images, call `POST /vision/part/generate` (`generatePartImagesFromOeNumber`) with an `Idempotency-Key` and `VisionPartGenerationRequest`:
   - `oeNumber` is required.
   - `angles` contains one to six values from `front`, `rear`, `left`, `right`, `left_45` and `right_45` when supplied.
   - `surfaceFinish=manufacturer_color` requires `colorCode`; supplying `colorCode` with `auto` selects that finish.
   - `referenceImageUrls` contains at most eight authorised URLs.
   - `sourceMode` is one of `references_required`, `references_preferred` or `knowledge_only`; `knowledge_only` must not be combined with reference URLs.
   - `generateBaseModel` defaults to `true`. Set it explicitly to `false` unless the shop needs the optional model asset and has a storage policy for it.
7. The generation call returns `202`. Retain the job reference and poll `GET /vision/part/generation-jobs/{jobId}` (`getPartImageGenerationJob`) until terminal. On success, validate every required `VisionPartGenerationResult` block: `format`, `oeNumber`, `coverage`, `basis`, `synthetic`, `disclosure`, `finish`, `part`, `dimensions`, `materials`, `paintableSurface`, `baseModel`, `views`, `identifierProtection`, `limitations` and `generatedAt`. Copy every required `views[].imageUrl` into the customer workflow and verify `views[].sha256`. When `baseModel.status=generated` and `baseModel.modelUrl` is non-null, process that asset too and verify `baseModel.sha256` when the field is present.
8. For a vehicle listing, only when the Client already owns a valid `tapiId`, call `POST /vehicles/{tapiId}/listing` (`composeVehicleListing`) with `{quality?, language?, notes?}` and an `Idempotency-Key`. `language` is `de`, `en` or `fr`. Persist `format`, `tapiId`, `language`, `title`, `description`, `highlights`, `documentedEquipmentCount` and `truncated`.

### Identifiers carried forward

| Identifier | Source | Destination and rule |
|---|---|---|
| Shop item ID / SKU | Shop | Local correlation only. |
| Raw OE input | Shop | Normalization audit; never overwrite it with the normalized result. |
| `normalizedOeNumber` | Normalization and base-part responses | All later OE calls after a confirmed match. |
| `tapiGenArt` and `vdi[]` | Base-part response | Classification fields only; not a substitute for the OE number. |
| `vehicleTypeKey` | Confirmed OE response `fitment[]` | Opaque type-level assignment key; it is neither VIN nor `tapiId` and has no public resolver in API 1.71.0. |
| `references[].manufacturer` and `references[].numbers[]` | Base-part response | Confirmed reference and comparison numbers in their returned manufacturer groups; retain no inferred direction, interchangeability or fitment claim. |
| `referenceNumbers.oe_oem_reference_numbers[]` | Base-part response | Consolidated confirmed OE/OEM references including the confirmed OE number; do not derive this list from `replacementChain`. |
| `jobId` / `statusUrl` | Image-generation `202` | `getPartImageGenerationJob` only. |
| `sha256` | Generated view | Verify the copied asset and deduplicate local media safely. |
| `tapiId` | Earlier Client-owned VIN flow | Optional vehicle-listing operation; never derive it from shop content. |

### Validation, errors, idempotency and tenant handling

- Stop or request review for `unresolved`, `ambiguous` or `invalid` normalization. `valid=true` alone proves syntax, not existence.
- A base-part `404` or ambiguous `422` blocks the automatic enrichment chain.
- In a validated `200`, treat an empty fitment list as “no assignments returned”, not as global negative proof. Do not manufacture additional assignments locally. Use `POST /vin/cart-check` for a specific VIN.
- Validate `references[]` as closed `{manufacturer, numbers}` groups and `referenceNumbers` as the closed `{oe_oem_reference_numbers}` object. Do not infer direction, interchangeability or fitment from either reference view, and do not derive either view from `replacementChain`.
- Read a zero price range through `result`: `no_listings_found` means no listing was usable, not that the part is worth nothing. A range is only evidenced when `result` is `priced`. If the evaluation could not be carried out at all, the endpoint answers with an error status rather than zeros. The returned currency is a filter, not evidence of conversion.
- Use the returned SEO content and live-availability metadata as publication gates. A successful response still requires editorial review before publication.
- Every generated view is synthetic. Retain `basis`, `disclosure`, identity-verification flags, `verificationConfidence`, skipped angles and limitations with the asset. Do not claim that it depicts the actual stock item's wear or condition.
- Use idempotency for image-generation and vehicle-listing POSTs. GET normalization, base data, price, SEO, translation and job polling must not receive the header.
- Run the complete item flow under one tenant key. The idempotency namespace and `tapiId` scope are per Client.

### Persistence fields

Subject to an approved data-class policy, model the following logical fields: tenant ID, shop item ID/SKU, raw and normalized OE values, manufacturer hint, normalization rule-set version/status/rule/confidence, `tapiGenArt`, VDI codes, policy-permitted base-part, fitment, reference-family and directed replacement facts, price request dimensions and response ranges, SEO request dimensions and returned publication/content fields, translation source text/language and result, image-generation request, job state, required generation provenance, copied asset locator and checksum, `tapiId` for vehicle listings, idempotency keys, operation IDs, OpenAPI version, timestamps, billing headers, publication-review decision and structured errors. Retain only the minimum approved subset; no complete payload or generated asset has an implied storage or redistribution right.

### Acceptance tests

1. `matched` normalization proceeds with the returned `normalizedOeNumber`; all other statuses stop before the base-part call.
2. The adapter accepts both populated and empty `fitment` lists, keeps every `criteria[]` value with its `vehicleTypeKey` and never manufactures assignments locally.
3. A reference or comparison number, replacement edge, aftermarket reference or VDI code never creates a compatibility claim by itself; returned `fitment[]` remains type-level evidence rather than a VIN-specific guarantee.
4. The price adapter preserves `condition`, `currency`, `vehicleType`, `result` and confidence with the range.
5. The SEO adapter accepts the documented publication fields, fills the listing title, category and item specifics from `content.ebayTitle`, `categoryId` and `itemSpecifics[]`, tolerates a `null` `categoryId` and an empty `itemSpecifics[]`, and handles optional live-availability metadata separately.
6. Image generation persists `202` before polling, resumes after restart and verifies each downloaded asset against `sha256`.
7. A generated base-model asset is copied to controlled storage and its checksum is verified when present; `generateBaseModel=false` creates no dependency on `modelUrl`.
8. `knowledge_only` plus `referenceImageUrls` is rejected before transmission.
9. The published image is visibly governed by the returned synthetic disclosure and never labelled as a stock photograph.
10. Replaying the generation request with its original idempotency key does not create a second local job or media set.

### Operational boundary

When the approved data policy permits copying an accepted generated asset, move it to controlled storage before publication and verify its checksum. Do not treat a returned asset URL as permanent storage.

## Recipe 4: parts inventory and dismantling

### Goal and non-goals

**Goal:** identify photographed parts, normalize candidate OE numbers, attach public part classifications, evaluate inventory and recall context, and optionally rank expected dismantling value for a VIN.

**Non-goals:** certifying a physical part's identity from OCR alone, proving that a reference fits a particular vehicle, replacing a dismantler's inspection, or turning an economic evaluation into an automatic purchase decision.

### Prerequisites

- A unique local stock-unit ID for every physical part and, when applicable, a local vehicle-unit ID.
- Authorised, publicly retrievable label or part-image URLs.
- A configured condition and `vehicleType` for pricing, and explicit economic assumptions for vehicle evaluation.
- A review queue for multiple OCR candidates, ambiguous normalization, incomplete VIN parts and recall matches.

### System-of-record boundary

The inventory or dismantling system owns the physical unit, provenance, serial/instance identifiers, measured condition, location, quantity, cost, sale state and disposal decision. The tapinomahub API provides OCR candidates, OE reference data, classification codes, indicative price data, series-level recall context and a modelled economic evaluation.

### Operation sequence

#### Part-unit intake

1. Call `POST /scanner/label/extract-partnumbers` (`extractLabelPartNumbers`) with `{imageUrl, quality?}` and an `Idempotency-Key`.
2. Read `partNumbers[]`; production and sandbox use the same response shape. Preserve every candidate and do not select one only because it appears first.
3. For each candidate, call `GET /parts/oe/normalize` (`normalizeOeNumber`) with `oeNumber` and an optional manufacturer hint. Proceed only with a confirmed `matched` result.
4. Call `GET /parts/oe/{oeNumber}` (`getOePart`) for the confirmed normalized number. Store `tapiGenArt`, `vdi[]`, `part`, `fitment`, `replacementChain`, `references` and `referenceNumbers` as distinct reference data linked to the physical stock unit. Preserve `part.name` as the returned string or `null`. Treat `tapiGenArt: null` and `vdi: []` as independent missing enrichments, not as a failed base lookup. Keep `criteria[]` attached to its opaque `vehicleTypeKey`. `fitment` contains the available type-level assignments and can be empty. Preserve grouped and consolidated references as returned, and never derive them from the replacement chain or turn them into an undirected substitution rule.
5. Load the classification catalogue with `GET /vdi` (`getVdiCatalog`). Prefer the unpaginated response when feasible. If paging is required, use `limit` from 1 to 500, advance `offset` by `page.returned`, and stop on `page.hasMore=false`. Version the local index by `stand.catalogVersion`. This catalogue does not return part mappings; use only VDI codes already returned by `getOePart` or by a successful VIN-parts position for the link.
6. Optionally call `GET /parts/oe/{oeNumber}/aftermarket-references` (`getOeAftermarketReferences`). These are reference records, not interchange or fitment guarantees. Prefer an unpaginated response; if paging, use `page.returned` and `page.hasMore`.
7. Optionally call `GET /parts/oe/{oeNumber}/price` (`getOePrice`) with explicit `condition`, `currency` and `vehicleType`.
8. For cluster-level market evidence, call `GET /parts/intelligence` (`resolvePartIntelligence`) with the confirmed OE number, explicit market dimensions and its required `Idempotency-Key`. On `202`, persist and poll only `GET /parts/intelligence/jobs/{jobId}` (`getPartIntelligenceJob`). On completion, keep the returned `clusterId`, `clusterVersion`, scope and snapshot metadata together.
9. With a known cluster, read current Intelligence through `GET /parts/clusters/{clusterId}/intelligence` (`getPartClusterIntelligence`) or materialized snapshots through `GET /parts/clusters/{clusterId}/history` (`getPartClusterIntelligenceHistory`). Both require their own idempotency key; history also requires `from` and `to` and never starts a job.
10. For recall context, batch one to 100 stock positions into `POST /recalls/parts` (`matchPartRecalls`). Send each as `{oeNumber, reference, vehicle?}` where `reference` is the local stock-unit key, and use an `Idempotency-Key`. Map results by returned `reference` and preserve register data versions and the scope notice.

#### Vehicle-level dismantling evaluation

1. Call `GET /vin/{vin}/economic-evaluation` (`getVinEconomicEvaluation`) with a known full VIN and the documented business parameters such as `country`, `condition`, `vehicleType`, `maxPricedParts`, `recoveryRate` and `costPerPart`.
2. On `200`, persist the result. On `202`, persist the job and poll `GET /vin/economic-evaluation/jobs/{jobId}` (`getVinEconomicEvaluationJob`).
3. Consume the echoed `assumptions`, `coverage`, `revenuePotential`, `purchaseRecommendation` and ranked `parts` together. Preserve `warnings` and `liveAvailability` when present; both are optional. Incomplete coverage makes the totals a lower bound.
4. If the workflow needs the underlying vehicle-related parts without an economic model, call `GET /vin/{vin}/parts` (`getPartsByVin`) and follow its `200`/`202` branch via `GET /vin/parts/jobs/{jobId}` (`getVinPartsJob`). Retain `matchLevel` and `missingCategories`.
5. When cluster-based valuation is required, call `POST /vin/economic-intelligence` (`createVinEconomicIntelligence`) with VIN, market country, currency, condition and a separate `Idempotency-Key`. On `202`, poll only `GET /vin/economic-intelligence/jobs/{jobId}` (`getVinEconomicIntelligenceJob`). Keep final `evaluationId`, pipeline, portfolio, candidates, confidence, provenance and warnings together; do not substitute this response for the legacy evaluation shape.

### Identifiers carried forward

| Identifier | Source | Destination and rule |
|---|---|---|
| Stock-unit reference | Inventory | `matchPartRecalls.positions[].reference`; returned unchanged and used for line mapping. |
| OCR candidate | Label extraction | Normalization input only; not yet a confirmed stock identity. |
| `normalizedOeNumber` | Normalization | Base part, references, price and recall request after `status=matched`. |
| `tapiGenArt` | Base-part or VIN-parts response | Stable tapinomahub classification linked to the stock unit; not an OE number. |
| `vdi[]` | Base-part or VIN-parts response | Required confirmed mapping array, including `[]`; join non-empty values to the separately versioned VDI catalogue. |
| VIN and `tapiId` | Vehicle flow | Keep both; VIN identifies the individual input and `tapiId` the technical type. |
| Economic `jobId` | `202` response | `getVinEconomicEvaluationJob` only. |
| `clusterId` and `clusterVersion` | Part or VIN Intelligence result | Current/history reads and snapshot correlation; never substitute an OE number. |
| Part Intelligence `jobId` / `statusUrl` | Resolver or current-snapshot `202` | `getPartIntelligenceJob` only. |
| VIN Intelligence `jobId` / `statusUrl` | VIN Intelligence `202` | `getVinEconomicIntelligenceJob` only. |
| `evaluationId` | Final VIN Intelligence response | Audit correlation only; no public lookup operation consumes it. |

### Validation, errors, idempotency and tenant handling

- OCR output is a candidate set. Require normalization and, where business risk warrants it, human confirmation against the physical label.
- Treat normalization `ambiguous` and `unresolved` as review states. Do not join on `lookupKey` as if it were a confirmed OE number.
- An empty VDI list means no confirmed mapping. An empty replacement chain means no documented replacement. Neither permits a locally invented classification.
- Because the aftermarket response has conflicting wording around `count` and paginated list length, do not use `count == aftermarketReferences.length` as a paging invariant. Use `page.returned` and `page.hasMore`, or omit pagination.
- Validate every VIN-parts success before consuming it. A response missing expected public fields must enter quarantine rather than producing an incomplete stock record.
- Recall results are series-level information as of the returned register data version. They do not prove whether a specific physical vehicle or part is affected or remediated.
- Economic output is valid only with its returned assumptions and coverage; retain and apply any `warnings` that are present. Do not compare evaluations that used different dimensions without normalising them in the inventory system.
- Use idempotency wherever the selected operation declares it. The three Intelligence data GETs require the header; their status polls do not. Serialize calls per tenant Client, including polls.
- Keep `observedSupply`, `verifiedSales` and `internalUsage` separate. Supply listings are not sales. Block automated pricing, dismantling or acquisition when `publishable=false`, evidence is insufficient, or confidence violates the configured review threshold.

### Persistence fields

Subject to an approved data-class policy, model the following logical fields: tenant ID, stock-unit and vehicle-unit IDs, a policy-permitted source image reference/checksum, OCR response variant and candidates, normalization input and approved result fields, confirmed normalized OE number, `tapiGenArt`, VDI codes and catalogue version, permitted part/replacement/reference facts, price dimensions and result, recall position reference/status/measures/register versions/notice, VIN, `tapiId`, parts `matchLevel` and completeness, cluster ID/version, market scope, `asOf`, snapshot ID, separated evidence blocks, prices, standtime, trend, scores and confidence, VIN Intelligence `evaluationId`, pipeline/portfolio/candidates/provenance/warnings, job family/state, idempotency keys, operation IDs, OpenAPI version, timestamps, billing headers, review decisions and structured errors. Retain only the minimum approved subset for its stated purpose and duration; this model is not permission to retain images or full responses.

### Acceptance tests

1. The adapter accepts both declared label-response property names and preserves each candidate as a separate value.
2. Two OCR candidates remain separate until each normalization result is evaluated.
3. A VDI catalogue refresh is keyed by `catalogVersion`; an OE mapping is never inferred from catalogue content.
4. Unpaginated and paginated classification loads produce the same ordered entry set; pagination stops by `page.hasMore`.
5. Recall batch responses map back to duplicate OE numbers by unique local `reference`, not by number alone.
6. A `202` economic job resumes after restart, and a nested failed status never creates a recommendation.
7. A response with incomplete coverage is labelled as a lower bound and cannot pass an automatic purchase threshold without the configured review rule.
8. Two tenants using the same stock-unit string remain isolated by Client key.
9. A resolver `202` survives restart and polls only the part-Intelligence status route; its `jobId` fails safely on the VIN status route.
10. History never creates a job or a synthetic missing point, and cluster version, snapshot ID, scope and `asOf` remain attached to every stored point.
11. Observed-supply counts never populate verified-sale counts, and `publishable=false` cannot trigger automated pricing or acquisition.
12. Final VIN polling yields an `evaluationId` equal to the preceding `jobId` and `requestId` in the current pilot; the adapter retains the three field roles and never generalises this intentional alias to another job family.

### Operational boundary

Retain submitted images, OCR results, jobs and evaluation evidence only as permitted by the approved policy. Do not rely on API-side history as the integrating system's recovery store.

## Recipe 5: vehicle intake

### Goal and non-goals

**Goal:** create a vehicle file from a registration-document URL, optionally add a visual condition report, preserve partial component outcomes and continue with technical data, recalls or listing text.

**Non-goals:** transferring ownership of the original document, performing a roadworthiness test, creating an expert appraisal, or treating `tapiId` as the physical vehicle's unique identifier.

### Prerequisites

- A local intake ID for the physical vehicle.
- An authorised public `fileUrl` and, optionally, one to five authorised `photoUrls` of the same vehicle.
- A two-letter `country` and an explicit decision whether holder data is necessary. Default to `includeOwner=false`.
- A tenant Client key and an idempotency key persisted before submission.

### System-of-record boundary

The intake system owns the physical vehicle, source documents and photos, consent/legal basis, custody, workflow status, human inspection, keeper-data policy and sale decision. The tapinomahub API returns a document extraction, matched vehicle-type data and an optional visual-only condition report.

### Operation sequence

1. Call `POST /vehicles/intake` (`intakeVehicle`) with `{fileUrl, photoUrls?, country?, includeOwner?}` and an `Idempotency-Key`.
2. A successful intake is synchronous HTTP `200`; this operation has no `202` branch. Validate `format`, `registrationDocument`, `vin`, `provider`, `tapiId`, `vehicle`, `conditionReport`, `components` and `complete`.
3. Evaluate each component independently:
   - `components.registrationDocument` is `delivered` on every successful response.
   - `components.vehicle` is `delivered`, `vin_not_readable` or `unavailable`.
   - `components.conditionReport` is `delivered`, `failed` or `not_requested`.
   - `complete=true` only when every requested component was delivered.
4. If `tapiId` is non-null, optionally call `GET /vehicles/{tapiId}` (`getVehicleByTapiId`) to retrieve the technical vehicle-type record.
5. If a new or larger photo set must be assessed independently, call `POST /vision/condition-report` (`reportVehicleCondition`) with one to eight `imageUrls`, optional `quality` and a new `Idempotency-Key`. Preserve `gradable`, `grade`, `reason`, all eight zones, `limitations`, `imageCount` and `visualOnly`.
6. If VIN is non-null, optionally call `GET /recalls/vehicles/{vin}` (`matchVehicleRecalls`) and preserve measures, register versions and the scope notice.
7. If `tapiId` is non-null and publication text is required, call `POST /vehicles/{tapiId}/listing` (`composeVehicleListing`) with `{quality?, language?, notes?}` and a separate `Idempotency-Key`; `language` is `de`, `en` or `fr`.

### Identifiers carried forward

| Identifier | Source | Destination and rule |
|---|---|---|
| Local intake ID | Intake system | Local correlation only and parent of every derivative record. |
| `vin` | Intake response | Recall or later VIN workflows only when non-null; retain separately from `tapiId`. |
| `tapiId` | Intake response | Technical type and listing operations only when non-null. |
| `provider` | Intake response | Preserve the exact numeric value returned by the API. |
| Component status | Intake response | Drives partial-success workflow and refund reconciliation; never replace with HTTP status alone. |

### Validation, errors, idempotency and tenant handling

- Verify that all photo URLs refer to the same vehicle before calling the API.
- Do not request holder data unless the intake purpose requires it. When `includeOwner=false`, keep the returned owner block null and do not enrich it elsewhere.
- A successful HTTP `200` can still be a partial vehicle file. Never require non-null `vin`, `tapiId`, `vehicle` or `conditionReport` without checking `components`.
- `visualOnly=true` means the condition report is not an expert appraisal, measurement or functional test. A null grade with `gradable=false` is a valid outcome; retain `reason` and `limitations`.
- Use separate idempotency keys for intake, a later condition report and listing generation. Retry the same timed-out write with its original key and identical body.
- Use the same tenant Client for the intake and every `tapiId` continuation. A `404` from a continuation can mean the reference is unknown or foreign; do not copy data across tenants.

### Persistence fields

Subject to an approved data-class policy, model the following logical fields: tenant ID, local intake ID, policy-permitted document/photo references and checksums, `includeOwner`, country, request hash, intake idempotency key, response format, approved registration-document fields, VIN, `tapiId`, numeric `provider`, approved vehicle and condition fields, each component status, `complete`, permitted later technical/recall/listing facts, separate idempotency keys, operation IDs, OpenAPI version, timestamps, billing headers, human-review status and structured errors. Retain only the minimum approved subset and block production persistence until document, photo and holder-data rules are approved.

### Acceptance tests

1. A complete sandbox intake validates all required response fields and links the local intake ID, VIN and `tapiId` without conflating them.
2. A fixture with `components.vehicle=vin_not_readable` persists the document result, leaves vehicle fields nullable and does not call VIN-dependent endpoints.
3. A fixture with `components.conditionReport=failed` records a partial success and does not discard delivered document or vehicle data.
4. `includeOwner=false` never produces a locally populated owner record.
5. A condition report with `gradable=false` and `grade=null` enters review and preserves all limitations.
6. Replaying the intake key produces one local intake result and recognises the idempotent response headers.
7. A `tapiId` obtained under one Client is not resolved using another tenant's key.

### Operational boundary

The intake system must remain the recovery and retention authority for the vehicle file, source document, photos, holder data and condition report. Apply an approved retention policy to every stored field.

## Recipe 6: multi-tenant middleware

### Goal and non-goals

**Goal:** provide a controlled server-side adapter that provisions isolated workspaces, routes calls with the correct key, serialises per-Client work, normalises retries and polling, and exposes usage to operations.

**Non-goals:** exposing the master key, accepting arbitrary pass-through paths, inventing endpoint keys, combining tenant data, or becoming the system of record for ERP, DMS, marketplace, inventory or vehicle workflows.

### Prerequisites

- A Master client API key kept in a privileged secret store and unavailable to normal business-call workers.
- A durable tenant registry with a unique local tenant ID and immutable `externalReference` matching `^[A-Za-z0-9._:-]{1,80}$`.
- A contractually supplied allowlist of valid `endpointKeys`. OpenAPI does not publish a complete operation-to-endpoint-key mapping, so middleware configuration must not derive one from paths.
- A per-Client queue, idempotency ledger, async-job store, secret manager and audit log.

### System-of-record boundary

The middleware owns Tenant-to-Client mapping, secret references, request correlation, idempotency state, queue state, job state and transport audit. The calling business system owns its source and result records. tapinomahub owns API authentication, Client-level billing/limits and the returned service result. The middleware must not merge results merely because two Tenants used the same VIN, OE number, `jobId` or local reference.

### Operation sequence

#### Tenant provisioning

1. Reserve the local `externalReference` in a pending provisioning record before any network call.
2. With the master key, call `POST /client/partner-workspaces` (`createPartnerWorkspace`) with no `Idempotency-Key`. Send:
   - required `name`, `externalReference` and one to 50 contract-approved `endpointKeys`;
   - optional `sponsorship` and `applicationLabel`.
   - Omit `rateLimits` in this call: OpenAPI exposes that array but does not define its item fields. Apply limits through the typed operation in step 4.
3. On `201`, validate that `workspace.id` and `workspace.externalReference` are present before activating the tenant; the top-level `workspace` is required but its inner properties are not marked required by the current schema. Then atomically persist those values, `apiKey.id`, a secret-manager reference to the one-time raw `apiKey.key`, `allowlist` and `sponsorshipGrant`. The `apiKey` object's `id`, `label`, `key` and `createdAt` are required. Never write the raw key to logs or ordinary database columns.
4. If limits were not final at provisioning, call `PUT /client/users/{clientId}/rate-limits` (`replaceClientUserRateLimits`) only after contractual clarification or controlled onboarding configuration has established the Client ID independently of `workspace.id`. Use the Master client key, `{rateLimits}` and an `Idempotency-Key`. Each `RateLimitInput` supplies `endpointKey`, `windowSeconds`, `maxRequests`, optional `apiKeyId` and optional `isActive`.
5. If sponsorship terms require a later explicit grant, call `PUT /client/sponsorship-grants/{grantReference}` (`upsertSponsorshipGrant`) with the Master client key, a stable `grantReference`, an explicitly sourced `beneficiaryClientId`, approved `endpointKeys` and optional contract fields. Use an `Idempotency-Key`; do not infer `beneficiaryClientId` from `workspace.id`.
6. If the beneficiary is authorised to choose sponsor terms, use its own tenant key for `PUT /client/sponsorship-grants/received/{grantReference}/billing-mode` (`chooseSponsorshipBillingMode`) with `{mode, sponsorClientId?}` and an `Idempotency-Key`.

#### Runtime routing

1. Authenticate the middleware caller and resolve exactly one local tenant record.
2. Reject any requested capability outside that Tenant's local allowlist before selecting a tapinomahub path.
3. Load only that Tenant's API-key secret and enqueue the call by the stable local Tenant record that represents exactly one tapinomahub Client, or by the explicitly established Client ID. Never partition concurrency by API-key ID: several keys can belong to one Client and must share its single queue. Then construct a request for an explicitly supported operation.
4. Obtain or create a durable business-operation idempotency key only when the selected operation declares it. This includes the three Intelligence data GETs; omit it from both Intelligence status polls and other GETs that do not declare it.
5. Apply the shared status/error policy. Persist a `202` before releasing the Client queue; schedule the documented poll through the same per-Client queue and matching tenant key.
6. Expose operational consumption using either `GET /client/usage` (`getClientUsage`) with the Tenant key or, only when the `{clientId}` mapping is explicitly established, `GET /client/users/{clientId}/usage` (`getClientUserUsage`) with the Master client key. `GET /client/credits` (`getClientCredits`) reports the authenticated Client's current balance or partner billing mode.

#### Key and Client lifecycle

1. Create an additional Tenant key with `POST /client/users/{clientId}/keys` (`createClientUserApiKey`) only for an explicitly established Client ID, using the Master client key and optional `{label}`. Do not send `Idempotency-Key`. Before cutover, require `apiKey.id`, `apiKey.label`, `apiKey.key` and `apiKey.createdAt`; neither the response wrapper nor `ApiKeyCreateResponse` currently marks these fields as required. If any is missing, enter `reconciliation_required` and do not repeat the call automatically. Store a complete raw key once as for initial provisioning.
2. Move traffic to the new secret only after a successful authentication test such as `GET /ping` (`getSystemStatus`).
3. Deactivate a Tenant with `PATCH /client/users/{clientId}` (`updateClientUser`) only for an explicitly established Client ID and `{isActive:false}` using an `Idempotency-Key`. Confirm subsequent Tenant-key calls are rejected before marking offboarding complete locally.

### Identifiers carried forward

| Identifier | Source | Destination and rule |
|---|---|---|
| Local tenant ID | Middleware | Primary partition key for every secret, request, result and job record. |
| `externalReference` | Middleware | One stable provisioning reference per end customer; duplicate submission returns `workspace_reference_exists`. |
| `workspace.id` candidate and returned `sponsorshipGrant.beneficiaryClientId` | Provisioning response | Keep both as distinct fields. Do not use `workspace.id` in a `{clientId}` path unless the public contract explicitly establishes that relationship. |
| `apiKey.id` | Provisioning/key response | Secret metadata and optional key-scoped rate limit; it is not the secret. |
| Raw `apiKey.key` | One-time success response | Secret manager only; never returned to logs or recoverable from later public operations. |
| `grantReference` | Sponsor | Sponsorship changes and beneficiary billing-mode selection. |
| Business idempotency key | Middleware | One Client, operation and canonical request; stored before dispatch. |
| `jobId` / `statusUrl` | Business `202` | Tenant-partitioned polling record for the exact documented job operation. |

### Validation, errors, idempotency and tenant handling

- Treat the API key as the effective tapinomahub Client selector. Never permit a caller to supply or override `X-Api-Key`.
- Enforce allowlisted operation IDs and paths locally; do not operate as a transparent arbitrary-path proxy.
- Provisioning and additional-key creation are not replay-safe. On timeout or lost response, mark provisioning `reconciliation_required` and do not call again automatically. A duplicate `externalReference` prevents a second partner workspace, but the public contract has no lookup-by-external-reference or raw-key recovery operation.
- `RateLimitInput` requires `endpointKey`, `windowSeconds` and `maxRequests` in its schema. Always send all three even though descriptions mention defaults. Validate allowed endpoint keys and windows against supplied contract configuration.
- On `429 client_request_in_progress`, retain the same tenant job and fix or drain its Client queue. Do not move the call to another tenant key.
- On idempotency `409`, apply the shared exact branches. Idempotency keys are per Client, so the same textual value in two Clients is not a cross-tenant correlation key.
- Keep master administration, tenant business calls and beneficiary billing-mode changes in separate credential scopes.

### Persistence fields

Subject to an approved data-class policy, model the following logical fields: local tenant ID, provisioning state, `externalReference`, separately sourced Client/workspace identifiers, parent/master relation, active state, allowed operation IDs, contract-supplied endpoint keys, API-key ID/label/created time and secret reference, never the raw key, rate-limit facts, sponsorship reference and state, billing mode, queue lease/state, request correlation ID, canonical request hash, operation ID/path/method, idempotency key and replay flags, HTTP status, job fields, retry schedule, response schema version, billing/warning headers, permitted usage facts, structured error and audit timestamps. Persist only the minimum approved subset; key-secret handling follows the secret-store rule independently of business-data retention.

### Acceptance tests

1. Provisioning returns one Client and stores the one-time raw key only in the secret manager; logs and database exports contain no secret.
2. A workspace success missing `workspace.id` or `workspace.externalReference` and an additional-key success missing raw key material both enter reconciliation rather than activation.
3. Provisioning omits untyped `rateLimits`; the subsequent typed replacement call persists the returned limit set.
4. An uncertain provisioning response enters reconciliation and is not automatically repeated.
5. A caller cannot override its resolved Client key or invoke an operation outside the local allowlist.
6. Two tenants run in parallel, while two requests for one Client execute serially.
7. A `202` job remains tenant-partitioned, survives restart and polls with the same Client key.
8. Every operation declaring idempotency has a durable pre-dispatch record; Intelligence data GETs carry the header and GET status polls do not.
9. `409 idempotency_key_conflict`, `409 idempotency_request_in_progress` and `409 idempotency_result_unavailable` reach distinct middleware states.
10. Once a Client ID is explicitly established, usage obtained with the Master client key for `{clientId}` is assigned to the same Tenant as usage obtained with that Tenant's own key; without that mapping, the master-side assertion remains blocked.
11. New-key cutover succeeds only after a tenant-key health call; deactivation makes the old tenant key fail authentication.

### Operational boundaries

Use only documented lifecycle operations and fields. Supply endpoint-key mappings and recovery procedures as controlled configuration, quarantine incomplete provisioning responses, and apply the calling system's authorised retention policy to business data.
