# Identifiers and data flow

This guide answers “where do I get this value?” and “where may I send it next?” for the identifiers exposed by the public [OpenAPI 3.1 contract](/specs/tapinoma-hub.en.json) (version 1.71.0). Field paths below start at a successful JSON response body unless a callback or request location is stated explicitly.

Treat every identifier as opaque unless its schema gives a format or pattern. Preserve the original string, case and separators when the contract does not promise normalization. Never derive one identifier type from another.

## VIN

| Question | Contract answer |
|---|---|
| **Meaning** | `vin` is the vehicle identification number used by the VIN and vehicle-recall operations. `extractVinFromImage` returns only a visibly photographed, complete and unambiguous 17-character VIN; the browser-session request separately permits a partial 3–17-character input so that a user can complete it in the interface. A VIN identifies the vehicle input; it is not a compatibility result and is not interchangeable with `tapiId`. |
| **Producers and exact response fields** | **Extraction:** `extractRegistrationDocument` — production `data.vehicle.vin`, sandbox `vehicle.vin`; its deprecated `extractRegistrationDocumentLegacyPath` alias returns the same field. `extractInternationalRegistrationDocument` and deprecated `extractInternationalRegistrationDocumentLegacyPath` — `vehicle.vin`; `extractCalculationDocument` and deprecated 11-field `extractDocument` — `vehicle.vin`; `extractVehicleDocument` and deprecated `extractVehicleDocumentLegacyPath` — `vehicle.vin`; `extractVinFromImage` — `vin`; `intakeVehicle` — `registrationDocument.vehicle.vin` and top-level `vin`. These fields may be null where their schemas permit it. **VIN-operation echoes/results:** `getVehicleByVin` — `vin`; `getPartsByVin` — immediate `vin`, accepted-job `vin`, successful async `result.vin`; `getVinPartsJob` — job `vin` and successful `result.vin`; `checkVinCart` — immediate `vin`, accepted-job `vin`, successful async `result.vin`; `getVinCartCheckJob` — job `vin` and successful `result.vin`; `getVinEconomicEvaluation` — immediate `vin`, accepted-job `vin`, successful async `result.vin`; `getVinEconomicEvaluationJob` — job `vin` and successful `result.vin`; `matchVehicleRecalls` — `vin`. |
| **Consumers** | Request body `vin` in `createVinRedirectSession` — `POST /vin/redirect-sessions`; path `vin` in `getVehicleByVin` — `GET /vin/{vin}/vehicle`; path `vin` in `getPartsByVin` — `GET /vin/{vin}/parts`; request body `vin` in `checkVinCart` — `POST /vin/cart-check`; path `vin` in `getVinEconomicEvaluation` — `GET /vin/{vin}/economic-evaluation`; request body `vin` in `createVinEconomicIntelligence` — `POST /vin/economic-intelligence`; path `vin` in `matchVehicleRecalls` — `GET /recalls/vehicles/{vin}`; optional request field `vehicle.vin` in `classifyEndOfLifeVehicle` — `POST /vision/vehicle/elv-classification`. |
| **Prerequisites** | The caller must be authorised to transmit and process the VIN. For `getPartsByVin`, use any optional numeric selection consistently; a conflicting value returns `409 vin_provider_mismatch`. `getVinEconomicEvaluation` accepts only the numeric selection enumerated in its schema. `createVinEconomicIntelligence` additionally requires market country, currency and condition and returns `evaluationId`, not a VIN echo, in its final response. Follow every operation's documented prerequisites. |
| **Alternatives** | Obtain a VIN from a registration document with `extractInternationalRegistrationDocument` or `extractRegistrationDocument`, from a calculation with `extractCalculationDocument`, from a rich vehicle document with `extractVehicleDocument`, or through the combined `intakeVehicle`; obtain it from a photograph with `extractVinFromImage`. The four `*LegacyPath`/`extractDocument` paths are compatibility aliases, not alternatives for new integrations. If only `tapiId` is available, the public API can return Client-scoped technical vehicle data or compose listing text, but it has no operation that converts `tapiId` back to VIN. |
| **Terminal/dead-end warnings** | A null scanner/intake VIN is not an identifier and must not be padded or guessed. The redirect callback returns no completed or normalized VIN; a 3–16-character session input therefore cannot feed parts, cart, economic or recall operations unless a full VIN is obtained independently. A cancelled callback has no `tapiId`. `incomplete=true` from `getVehicleByVin` means the VIN was not resolved to exactly one vehicle; remaining fields describe only the first candidate. If recall information cannot be resolved for a VIN, that operation returns `404`. |

## `tapiId`

| Question | Contract answer |
|---|---|
| **Meaning** | `tapiId` is the stable public vehicle reference returned after a successful VIN flow or a successful vehicle-intake match. It refers to the matched technical vehicle type, not the individual physical vehicle; several VINs may share it. Access is Client-scoped. Store it separately from VIN. |
| **Producers and exact response fields** | `getVehicleByVin` — `tapiId`; completed `createVinRedirectSession` flow — callback query parameter `tapiId` (not a session-creation JSON field); `getPartsByVin` — immediate `tapiId` or successful async `result.tapiId`; `getVinPartsJob` — successful `result.tapiId`; `getVinEconomicEvaluation` — immediate `tapiId` or successful async `result.tapiId`; `getVinEconomicEvaluationJob` — successful `result.tapiId`; `intakeVehicle` — nullable top-level `tapiId`. `getVehicleByTapiId` and `composeVehicleListing` echo the consumed value as top-level `tapiId`. |
| **Consumers** | Path `tapiId` in `getVehicleByTapiId` — `GET /vehicles/{tapiId}`; path `tapiId` in `composeVehicleListing` — `POST /vehicles/{tapiId}/listing`. No other public request field consumes it. |
| **Prerequisites** | The reference must previously have been returned through a successful VIN flow or as a non-null `intakeVehicle.tapiId` for the same authenticated Client. A browser callback produces it only with `status=completed`; an async VIN job produces it only inside a successful `result`. For vehicle intake, use the returned `tapiId` only when it is non-null; do not infer one from other response fields. |
| **Alternatives** | If `tapiId` is unavailable but VIN is available, run the applicable VIN vehicle flow. `intakeVehicle` can combine document extraction and VIN matching, but its `tapiId` remains nullable. |
| **Terminal/dead-end warnings** | `tapiId=null`, a cancelled callback, or a failed job cannot feed a `{tapiId}` path. An unknown, foreign or unavailable value returns `404`. `GET /vehicles/{tapiId}` returns no VIN or equipment, and there is no reverse-lookup endpoint. Do not treat shared `tapiId` equality as proof that two records are the same physical vehicle. |

## OE number

| Question | Contract answer |
|---|---|
| **Meaning** | An OE number is the part-number string submitted through fields named `oeNumber` or `oeNumbers`. A scanner reading is only a candidate until an OE operation confirms it. Preserve the business input separately from any normalized result. |
| **Producers and exact response fields** | **Vehicle-derived:** `getPartsByVin` and `getVinPartsJob` — immediate or successful async `parts[].number` / `result.parts[].number` (nullable); `getVinEconomicEvaluation` and its job status — immediate or successful async `parts[].number` / `result.parts[].number`. **Confirmed OE base:** `getOePart` — `oeNumber`, `part.number`, grouped `references[].numbers[]`, consolidated `referenceNumbers.oe_oem_reference_numbers[]`, and directed `replacementChain[].from` / `replacementChain[].to`. **Normalization candidates:** `normalizeOeNumber` — original `input`, `normalizedOeNumber`, `equivalentOeNumbers[]`, and `knownCandidates[].value`. **Image readings:** `extractLabelPartNumbers` — `partNumbers[]`; `extractLabelDetails` — `data.primaryPartNumber` and `data.otherPartNumbers[]`. Production and sandbox use these same shapes. **Operation echoes:** `getOeAftermarketReferences`, `getOePrice` and `getPartSeo` — `oeNumber`; `checkVinCart` and its job status — successful `results[].oe` / `result.results[].oe`; `matchPartRecalls` — `positions[].oeNumber`; `generatePartImagesFromOeNumber` and its status — `oeNumber` and successful `result.oeNumber`. |
| **Consumers** | Path `oeNumber` in `getOePart` — `GET /parts/oe/{oeNumber}`; `getOeAftermarketReferences` — `GET /parts/oe/{oeNumber}/aftermarket-references`; `getOePrice` — `GET /parts/oe/{oeNumber}/price`; `getPartSeo` — `GET /parts/oe/{oeNumber}/seo`. Query `oeNumber` in `normalizeOeNumber` — `GET /parts/oe/normalize`; query `oeNumber` in `resolvePartIntelligence` — `GET /parts/intelligence`. Body `oeNumbers[]` in `checkVinCart` — `POST /vin/cart-check`; body `positions[].oeNumber` in `matchPartRecalls` — `POST /recalls/parts`; body `oeNumber` in `generatePartImagesFromOeNumber` — `POST /vision/part/generate`. |
| **Prerequisites** | Validate a scanner reading with `normalizeOeNumber` or `getOePart` before treating it as a catalogue fact. Send the optional `manufacturer` only when it is known. Cart checking additionally requires a VIN and `mode`; recall matching accepts optional vehicle context; image generation applies its own documented reference-evidence mode. |
| **Alternatives** | If the number is only visible on a label, use a label extraction operation first. If the business goal is parts for a vehicle and only VIN is known, use `getPartsByVin`. `tapiGenArt` and VDI codes are separate output classifications and cannot replace an OE-number request input. |
| **Terminal/dead-end warnings** | OCR strings, reference or comparison numbers, aftermarket references and SEO reference matches do not prove technical interchangeability or installation. `getOePart` returns `404` without a confirmed complete base match. A replacement edge is directed from `from` to `to`; do not invent the inverse or derive direction from `references` or `referenceNumbers`. In a cart response, `fits=false` is a reliable exclusion only when `complete=true`. |

## `normalizedOeNumber`

| Question | Contract answer |
|---|---|
| **Meaning** | `normalizedOeNumber` is the confirmed normalized OE number returned by the API, not a local formatting transform. In normalization it is non-null only for `status=matched`; in the base OE response it is the confirmed normalized number to which the response applies. |
| **Producers and exact response fields** | `normalizeOeNumber` — `normalizedOeNumber`; `getOePart` — `normalizedOeNumber`; `matchPartRecalls` — `positions[].normalizedOeNumber`. |
| **Consumers** | No public request schema has a field named `normalizedOeNumber`. The normalization schema states that a matched value is used for further work; pass it through an operation's `oeNumber` input when that is the intended next step. |
| **Prerequisites** | For `normalizeOeNumber`, require `status=matched` before consuming the field. For `matchPartRecalls`, require a non-null position value. A base OE `200` already represents a confirmed match. |
| **Alternatives** | `getOePart` can return a confirmed normalized value together with the base part. When explicit status, reasons, equivalent numbers or candidates are required, use `normalizeOeNumber` instead. |
| **Terminal/dead-end warnings** | The normalization response is null for `unresolved`, `ambiguous` and `invalid`. Do not implement uppercase/strip logic and label it `normalizedOeNumber`. |

## `part.name`

| Question | Contract answer |
|---|---|
| **Meaning** | Nullable public part designation for the confirmed OE base response. The value is a string or `null`; the public contract makes no guarantee about its language or origin. |
| **Producer and exact response field** | `getOePart` — `part.name`. When available, its value is a string or `null`. |
| **Consumers** | Catalogue, ERP and display integrations may store or render the returned value. Translation can receive the non-null value through `translatePartName` when its separate single-designation input contract is satisfied. |
| **Prerequisites** | Consume only after a successful `getOePart` base match and preserve the returned value exactly. |
| **Alternatives** | When `null`, request human curation or leave the local label empty. Do not reconstruct it from another response field. |
| **Terminal/dead-end warnings** | `part.name` describes a part but does not prove fitment, interchangeability or VIN-specific installation. |

## `tapiGenArt`

| Question | Contract answer |
|---|---|
| **Meaning** | `tapiGenArt` is the stable public tapinomahub GenArt attached to a returned part, represented as a string matching `^TGA-[0-9]{6,}$`. In `getOePart` the required field is `null` when no valid classification is available. It is opaque and distinct from OE numbers and VDI codes. |
| **Producers and exact response fields** | `getOePart` — top-level required `tapiGenArt` containing a valid code or `null`; `getPartsByVin` — immediate `parts[].tapiGenArt` or accepted job's successful `result.parts[].tapiGenArt`; `getVinPartsJob` — successful `result.parts[].tapiGenArt`. Every successful VIN-parts position contains exactly one non-null value. |
| **Consumers** | None. No public path, query parameter or request body field accepts `tapiGenArt`. |
| **Prerequisites** | Obtain a non-null value from a successful confirmed OE base response or a returned VIN-parts position. Do not create it from a name, numeric suffix or another classification value, and do not treat `null` in a confirmed OE response as a failed base match. |
| **Alternatives** | Use the OE number for OE operations and `vdi[]` for confirmed VDI mappings. These identifiers describe different contract concepts and are not substitutions for `tapiGenArt`. |
| **Terminal/dead-end warnings** | It is output-only in the released API. There is no public “parts by `tapiGenArt`” or “vehicles by `tapiGenArt`” operation. Treat the entire string as opaque; do not convert its suffix to an integer. The independently returned `vdi[]` field is a separate classification and must not be derived from `tapiGenArt`. |

## VDI

| Question | Contract answer |
|---|---|
| **Meaning** | A VDI 4081 code identifies a catalogue item. `getVdiCatalog` returns a base code matching `^VDI4081_[0-9]+$`. A confirmed part mapping in `getOePart.vdi[]`, `getPartsByVin.parts[].vdi[]` or `getVinPartsJob.result.parts[].vdi[]` may additionally have one numeric installation-location suffix and matches `^VDI4081_[0-9]+(?:_[0-9]+)?$`. |
| **Producers and exact response fields** | `getVdiCatalog` — `entries[].code` for base codes, plus `stand`, `mainGroups[]` and entry metadata; `getOePart` — required top-level `vdi[]` containing only valid confirmed full codes associated with the confirmed OE base record; `getPartsByVin` — required immediate `parts[].vdi[]` or a successful job's `result.parts[].vdi[]`; `getVinPartsJob` — required `result.parts[].vdi[]` on success. |
| **Consumers** | None. No public path, query or body field accepts a VDI code. |
| **Prerequisites** | Use `getOePart` after a confirmed OE base match when a mapping for one known OE number is required. Use the required `vdi[]` field of each returned VIN-parts position when the mapping belongs to that VIN result. Use `getVdiCatalog` with pagination when catalogue metadata is required. |
| **Alternatives** | `tapiGenArt` is a separate tapinomahub part-category identity and OE number is a separate part identifier. Neither is a documented conversion of a VDI code. |
| **Terminal/dead-end warnings** | `vdi=[]` means no valid mapping was available; it does not invalidate the part and does not leave a VIN-parts job incomplete. `getVdiCatalog` explicitly returns no OE assignments. A catalogue base code omits any installation-location mask; do not invent, append or strip a suffix to create an undocumented mapping. |

## `vehicleTypeKey`

| Question | Contract answer |
|---|---|
| **Meaning** | `vehicleTypeKey` is the opaque string key of one type-level assignment in `PartOeResponse.fitment[]`. It is neither a VIN nor a `tapiId`. Keep every `criteria[]` value attached to the key that carries it. |
| **Producers and exact response fields** | `getOePart` — `fitment[].vehicleTypeKey`. The same entry's `criteria[]` contains its documented restrictions; an empty list means no restriction is recorded for that assignment. |
| **Consumers** | None in API 1.71.0. No public path, query parameter or request body accepts `vehicleTypeKey`. |
| **Prerequisites** | Consume it only from a successful base OE response. `fitment=[]` means that no assignments were returned. |
| **Alternatives** | For one known vehicle, use VIN plus `POST /vin/cart-check`. For Client-scoped technical vehicle data, obtain a `tapiId` through a documented VIN flow and call `GET /vehicles/{tapiId}`. Neither operation converts `vehicleTypeKey`. |
| **Terminal/dead-end warnings** | The API does not resolve the key to make, model, VIN or `tapiId`, and it does not promise worldwide assignment completeness. Do not parse the key, join it to an undocumented table or treat it as installation approval. |

## `clusterId`

| Question | Contract answer |
|---|---|
| **Meaning** | `clusterId` is the UUID identity of one versioned part cluster. `clusterVersion` identifies the returned composition of that cluster. Neither value is an OE number, aftermarket number, fitment result, kType, NatCode or GenArt. |
| **Producers and exact response fields** | `resolvePartIntelligence`, `getPartClusterIntelligence` and the final `getPartIntelligenceJob` response — `cluster.clusterId` and `cluster.clusterVersion`; `getPartClusterIntelligenceHistory` — top-level `clusterId` and each `points[].clusterVersion`; `createVinEconomicIntelligence` and the final `getVinEconomicIntelligenceJob` response — `candidates[].clusterId` and `candidates[].clusterVersion`. |
| **Consumers** | Path `clusterId` in `getPartClusterIntelligence` — `GET /parts/clusters/{clusterId}/intelligence`; path `clusterId` in `getPartClusterIntelligenceHistory` — `GET /parts/clusters/{clusterId}/history`. The current-snapshot operation can additionally consume the exact returned `clusterVersion` as a query parameter. |
| **Prerequisites** | Obtain the UUID from an Intelligence result and keep `clusterVersion`, market scope, `asOf` and `provenance.snapshotId` with the result that produced it. A resolver request needs an OE or aftermarket identity anchor; kType, NatCode and GenArt are only disambiguation context. |
| **Alternatives** | With only an OE or aftermarket number, call `resolvePartIntelligence`. With only a VIN, call `createVinEconomicIntelligence` and use its returned candidate clusters. |
| **Terminal/dead-end warnings** | A cluster UUID does not prove technical interchangeability or VIN fitment. A newer cluster version must not silently rewrite a stored snapshot. Evidence and results are scoped to the authenticated Client and returned market dimensions; matching UUID text does not make tenant-scoped evidence portable. |

## `evaluationId`

| Question | Contract answer |
|---|---|
| **Meaning** | `evaluationId` is the UUID of one VIN Economic Intelligence evaluation. In the current pilot, the same UUID identifies the originating request and, during asynchronous processing, its job; the final response therefore has `evaluationId === requestId === jobId` for that workflow. |
| **Producer and exact response field** | `createVinEconomicIntelligence` on immediate completion, or the final `getVinEconomicIntelligenceJob` response — top-level `evaluationId`. An accepted or still-running `202` exposes the same UUID as `requestId` and `jobId`, but does not yet contain an `evaluationId` field. |
| **Consumers** | While the evaluation is pending, pass the same UUID under the `jobId` path parameter to `getVinEconomicIntelligenceJob`. No separate lookup path consumes an `evaluationId` field in API 1.71.0. |
| **Prerequisites** | Retain it only from a schema-valid final response and keep it with `asOf`, scope, pipeline, candidate set, confidence, provenance and warnings. |
| **Alternatives** | Use the returned `statusUrl` while work is pending and retain the field roles even though `requestId`, `jobId` and the later `evaluationId` intentionally contain the same UUID in this pilot. |
| **Terminal/dead-end warnings** | The equality is specific to the VIN Intelligence pilot contract. Never move the UUID to another job family or substitute it for `clusterId`, VIN or a local vehicle ID. |

## `jobId`

| Question | Contract answer |
|---|---|
| **Meaning** | `jobId` is a UUID identifying one asynchronous job family. It is not a general resource identifier and must be polled through the status operation paired with its producer. |
| **Producers and exact response fields** | `getPartsByVin` `202` — `jobId`; `checkVinCart` `202` — `jobId`; `getVinEconomicEvaluation` `202` — `jobId`; `generatePartImagesFromOeNumber` `202` — `jobId`; `resolvePartIntelligence` or `getPartClusterIntelligence` `202` — `jobId`; `createVinEconomicIntelligence` `202` — `jobId`. For both Intelligence families, `requestId` and `jobId` are the same UUID. A pending Intelligence status response repeats that UUID; the final VIN result exposes it again as `requestId` and `evaluationId`. |
| **Consumers** | `getVinPartsJob` — `GET /vin/parts/jobs/{jobId}`; `getVinCartCheckJob` — `GET /vin/cart-check/jobs/{jobId}`; `getVinEconomicEvaluationJob` — `GET /vin/economic-evaluation/jobs/{jobId}`; `getPartImageGenerationJob` — `GET /vision/part/generation-jobs/{jobId}`; `getPartIntelligenceJob` — `GET /parts/intelligence/jobs/{jobId}`; `getVinEconomicIntelligenceJob` — `GET /vin/economic-intelligence/jobs/{jobId}`. |
| **Prerequisites** | Create the value only by receiving the corresponding `202`. Store the required full `statusUrl`, job family and retry timing together; also capture `Location` for comparison, but do not construct or origin-resolve a URL from its current root-relative example. Poll `queued` or `running` jobs after the returned `Retry-After`/`retryAfterSeconds`. |
| **Alternatives** | Follow `statusUrl` rather than constructing a path. For VIN parts, cart checks, economic evaluations and Intelligence requests, an immediate `200` contains the result and creates no job to poll. Part-image generation is documented as asynchronous and starts with `202`. |
| **Terminal/dead-end warnings** | Use every `jobId` only with its returned `statusUrl` and paired status operation. Older job resources can return HTTP `200` while their domain state is still non-terminal; Intelligence status operations return `202` while pending and `200` only with the final business result. Never send a job ID to another family's path. |

## `clientId`

| Question | Contract answer |
|---|---|
| **Meaning** | `clientId` addresses a Client account. Every API key belongs to exactly one Client, and billing, credit, plans and rate limits attach to that Client. In `ClientUser` responses, the same Client identifier is spelled `id`, not `clientId`. |
| **Producers and exact response fields** | `listClientUsers` — `users[].id`; `createClientUser` and `updateClientUser` — `user.id`; `replaceClientUserRateLimits` — `rateLimits[].clientId`; `getClientUserUsage` and `getClientUsage` — top-level `clientId` and `balance.clientId`; `getClientCredits` — `clientId`; `listSponsorshipGrants` and `listReceivedSponsorshipGrants` — `sponsorshipGrants[].sponsor.clientId` and `.beneficiary.clientId`; grant mutation/choice responses — `sponsorshipGrant.sponsor.clientId` and `.beneficiary.clientId`. `createPartnerWorkspace` separately returns `workspace.id` and optional `sponsorshipGrant.beneficiaryClientId`; the contract does not normatively declare the former to be a `{clientId}` alias. |
| **Consumers** | Path `clientId` in `updateClientUser` — `PATCH /client/users/{clientId}`; `createClientUserApiKey` — `POST /client/users/{clientId}/keys`; `replaceClientUserRateLimits` — `PUT /client/users/{clientId}/rate-limits`; `getClientUserUsage` — `GET /client/users/{clientId}/usage`. The same identifier concept is consumed as body `beneficiaryClientId` by `upsertSponsorshipGrant` and optional body `sponsorClientId` by `chooseSponsorshipBillingMode`. |
| **Prerequisites** | Managed Sub-user operations require the authenticated Master client and a managed Sub-user. Sponsorship operations apply sponsor/beneficiary roles as documented by the selected operation. |
| **Alternatives** | `externalReference` is the integrator's stable key for `createPartnerWorkspace`; it prevents duplicate provisioning in that workflow but cannot replace a `{clientId}` path, and there is no public lookup by that value. Retrieve existing managed IDs with `listClientUsers`. |
| **Terminal/dead-end warnings** | Do not confuse `ClientUser.id`, `workspace.id`, `apiKey.id`, plan IDs, rate-limit record IDs or sponsorship-grant IDs. Do not search only for a response field literally named `clientId`: Client creation returns `user.id`. Do not send `workspace.id` into a `{clientId}` path unless the public contract explicitly establishes that relationship. A Client ID does not authorise cross-Client access by itself. |

## `grantReference`

| Question | Contract answer |
|---|---|
| **Meaning** | `grantReference` is the sponsor-selected external reference used to address and idempotently replace a sponsorship grant. It is 1–100 characters matching `^[A-Za-z0-9._:-]+$` and is unique per sponsor. The response spelling is `reference`, not `grantReference`. |
| **Producers and exact response fields** | The sponsor is the original producer because it chooses the path value. `listSponsorshipGrants` and `listReceivedSponsorshipGrants` return it as `sponsorshipGrants[].reference`; `upsertSponsorshipGrant`, `revokeSponsorshipGrant` and `chooseSponsorshipBillingMode` return it as `sponsorshipGrant.reference`. `createPartnerWorkspace` may also return `sponsorshipGrant.reference`. There is no JSON response field named `grantReference`. |
| **Consumers** | Path `grantReference` in `upsertSponsorshipGrant` — `PUT /client/sponsorship-grants/{grantReference}`; `revokeSponsorshipGrant` — `DELETE /client/sponsorship-grants/{grantReference}`; `chooseSponsorshipBillingMode` — `PUT /client/sponsorship-grants/received/{grantReference}/billing-mode`. |
| **Prerequisites** | Grant issue/replacement and revocation are sponsor operations. Billing-mode choice is a beneficiary operation. When several sponsors use the same text reference for one beneficiary, `chooseSponsorshipBillingMode` additionally requires body `sponsorClientId`. |
| **Alternatives** | List issued or received grants to recover the response `reference`. The returned grant `id` is informational; public mutation paths do not address a grant by that ID. `externalReference` identifies partner provisioning and is not a general substitute. |
| **Terminal/dead-end warnings** | Reusing a reference under the same sponsor replaces the grant; it does not create a second grant. The same text can be ambiguous from a beneficiary's perspective when used by several sponsors. Revocation stops new covered requests but does not delete existing results or entitlements. Active and historical grants remain listable; inspect `active`, dates and revocation state rather than treating presence as active coverage. |

## `state`

| Question | Contract answer |
|---|---|
| **Meaning** | `state` is optional caller-owned context for the browser-guided VIN flow, for example a cart identifier. It is a string or null with a maximum of 512 characters and is returned unchanged to the callback URL. An empty value counts as unset. |
| **Producers and exact response fields** | The caller is the only producer: send request body `state` to `createVinRedirectSession`. The session-creation JSON response has no `state` field. If set, the later browser redirect includes query parameter `state` on both completed and cancelled callbacks. This callback parameter is not a JSON response field. |
| **Consumers** | Only the caller's `returnUrl` handler consumes it to recover its own context. No public tapinomahub operation accepts a path, query or body field named `state` after session creation. |
| **Prerequisites** | Create the session server-side with body `vin`, an absolute HTTPS `returnUrl` without credentials or fragment, and optional `state`. Redirect the user's browser to the returned single-use `redirectUrl`; do not send the API key to the browser. |
| **Alternatives** | Omit `state` and correlate the callback entirely in the caller's own application state. The public API defines no separate session identifier field in `VinRedirectSessionResponse`; it returns `redirectUrl` and `expiresAt`. |
| **Terminal/dead-end warnings** | Empty or null `state` is omitted from the callback. `state` does not indicate completion: branch separately on callback `status`. A cancelled callback contains no `tapiId`; a completed callback contains no vehicle, equipment or access data beyond the documented query values. Do not expect `state` in the creation response or send it to another API endpoint. |

## Minimal hand-off rules

1. Record the operation that produced each value and its exact response path.
2. Record nullability and state gates such as `status=matched`, `status=succeeded` and `status=completed` with the value.
3. Pass a value only to a consumer listed in its section; otherwise stop because the public contract does not define that use.
4. Keep VIN, `tapiId`, `vehicleTypeKey`, OE number, `normalizedOeNumber`, `tapiGenArt`, VDI code, `clusterId`, `clusterVersion`, `evaluationId`, `jobId`, `clientId`, `grantReference` and `state` as distinct semantic fields in the integrating system. The current VIN Intelligence pilot intentionally places the same UUID in `requestId`, `jobId` and the later `evaluationId`; do not generalise that alias to another workflow.
