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. Separate each Tenant by giving it its own Client with dedicated API keys, normally a Sub-user created with
createClientUseror a partner workspace created withcreatePartnerWorkspace, and keep tenant identifiers only in the integrating system. - 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
tapiIdis usable only by the Client for which a VIN flow previously completed successfully. An unknowntapiId, or one not assigned to the authenticated Client, returns404vehicle_not_found. - 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
errorvalue second. Do not branch on the human-readablemessage. - Treat
400,404and422as data or workflow decisions. Correct or reconcile the input before another attempt. - Treat
401and403as authentication, authorisation, Client-state or sandbox-availability decisions. Do not rotate or replace a key automatically. - Treat
402as a billing or coverage decision. Do not loop while the plan, balance or sponsorship state is unchanged. - For
409, branch on the documentederror. In particular, never hidevin_provider_mismatch,redirect_requiredor an idempotency conflict behind a generic retry. - For
429, honourRetry-After.client_request_in_progressalso means the Client queue is not correctly serialised. - Retry
500,502and503only when the corresponding error-catalogue entry is retryable, and use a bounded backoff. A write retry must reuse its originalIdempotency-Key. - The contract states that a pre-charged individual call ending with HTTP status 400 or above is generally refunded automatically; combined workflows and contract-specific conditions follow the relevant service description or agreement. A refund does not turn a failed result into a success.
- Capture
X-Tapinoma-Billing-Source,X-Tapinoma-Billing-Bundle,X-Tapinoma-Sponsorship-GrantandX-Tapinoma-Usage-Warningwhen present. Warning text is for people and is not stable program input.
Idempotency rules
- Send
Idempotency-Keyonly to operations that declare it and follow that operation's schema. Intelligence explicitly requires it onGET /parts/intelligence,GET /parts/clusters/{clusterId}/intelligenceandGET /parts/clusters/{clusterId}/historyas 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.
X-Tapinoma-Idempotency-Stored: truemeans an unchanged repeat with the same key can return the earlier result;falsemeans the same key returns409 idempotency_result_unavailable. A replay reportsX-Tapinoma-Idempotent-Replay: trueand causes no additional charge. Keys are scoped per Client for 24 hours.- On
409 idempotency_request_in_progress, wait forRetry-Afterand repeat with the same key. On409 idempotency_key_conflict, stop because the key was used for different input. On409 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 central409states or a 24-hour retention promise. createClientUser,createClientUserApiKeyandcreatePartnerWorkspaceissue a raw API key and rejectIdempotency-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
202is an accepted job, not a business result. PersistjobId,statusUrl, the timestamp from the202body (createdAt;requestedAtin the two Intelligence families, whose202body also carriesrequestId), the originating operation and the original business key before acknowledging the request locally. - Honour both the
Retry-Afterheader and theretryAfterSecondsbody value. If both are present and differ, wait for the longer interval and record the discrepancy. Poll the required absolutestatusUrl. For VIN parts, cart check and VIN economic evaluation,Locationis typed as a URI reference and its documented example is root-relative; the other job families type it as an absolute URI. ResolveLocationagainsthttps://api.tapinomahub.combefore comparing it withstatusUrl, and quarantine the job on a mismatch instead of choosing one address. Do not resubmit the initiating operation merely because it returned202. - Continue for
queuedandrunning. Older status resources can express those states inside HTTP200; the two Intelligence status operations return HTTP202while pending and HTTP200with the final result. Require the workflow's usable result before completion and quarantine invalid state/result combinations. A requiredvdiarray 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
providervalue permitted by the Client's contract. - For browser-assisted route
1, an HTTPSreturnUrlwithout credentials or fragment and a server-side correlation store. The ERP should generate an opaquestateand 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
- Optionally verify connectivity with
GET /ping(getSystemStatus). Do not sendIdempotency-Keyto this read operation. - Establish vehicle context with exactly one of these entry branches:
- Direct server-to-server branch: call
GET /vin/{vin}/vehicle(getVehicleByVin) with explicitprovider=2orprovider=3, the two-lettercountry, and explicit choices for the optionalincludeEquipments,includeColorsandincludeTechnicalflags, or deliberately accept theirtruedefaults. Carry forward the returnedvin,tapiId,providerandincompletevalues. - Browser-assisted branch: server-side, call
POST /vin/redirect-sessions(createVinRedirectSession) with a 3–17-charactervininput, HTTPSreturnUrland an opaquestate. Use anIdempotency-Key. Redirect only the user agent to the returnedredirectUrlbeforeexpiresAt. On the callback, require the originalstateand, as a local anti-forgery control, reject a state already consumed by the ERP; accepttapiIdonly whenstatus=completed. A cancellation has notapiId. The callback does not contain the VIN completed in the browser. Fetch technical type data withGET /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.
- Direct server-to-server branch: call
- Only with a known full 17-character VIN, retrieve parts with
GET /vin/{vin}/parts(getPartsByVin). Omit the optionalproviderquery parameter unless the integration explicitly needs it; an explicit value that differs from the selection value associated with the VIN returns409 vin_provider_mismatch. The responseproviderstates the numeric selection used for the result, and a202job is documented only forprovider=1.- On
200, consumevin,tapiId,provider,matchLevel,allCategorySigns,missingCategoriesandparts; every part contains requiredtapiGenArtandvdifields, andvdi=[]is valid when no mapping is available. - On
202, persistjobId,statusUrl,vin,provider,createdAtand retry timing, then pollGET /vin/parts/jobs/{jobId}(getVinPartsJob).
- On
- If the ERP needs cart verification, call
POST /vin/cart-check(checkVinCart) with{vin, country?, mode, oeNumbers}and anIdempotency-Key.modeis exactlytypeorvehicle; preserve request order and duplicates.- On
200, mapresultsback to cart lines by array position and keepcompletewith every result. - On
202, which the contract documents only formode=vehicle, persist the returned cart-checkjobId,statusUrlandcreatedAt, then poll onlyGET /vin/cart-check/jobs/{jobId}(getVinCartCheckJob).
- On
- Optionally read the Client's current balance, plan consumption and endpoint-specific 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,tapiIdorprovider; the currentVinVehicleResponseschema does not mark these properties as required. - Likewise, validate a
VinPartsResponsebefore use. Quarantine a successful payload that lacksvin,provider,matchLevel,allCategorySigns,missingCategoriesorparts; the current schema describes these top-level properties but marks none of them as required.tapiIdis explicitly nullable. Every returned part must contain a validtapiGenArtand avdiarray; preserve an empty array instead of treating the position or job as incomplete. - A direct vehicle request with
provider=1can return409 redirect_required; start the documented redirect flow instead of retrying the same direct request. 409 vin_provider_mismatchmeans an explicitproviderdiffers from the selection value associated with the VIN. Do not repeat the request unchanged; omitprovideror change it only through an explicit configuration decision, never silently.incomplete=truemeans the vehicle response is not an exact single-vehicle resolution.matchLevelmust remain attached to the parts data.- A non-empty
missingCategorieslist means the parts list is incomplete. In a cart response,fits=falsecombined withcomplete=falseis not a definitive exclusion. - Use idempotency for redirect-session creation and cart checks.
getVehicleByVin,getPartsByVin,getVehicleByTapiId,getVinPartsJobandgetVinCartCheckJobdeclare noIdempotency-Keyand reject it with400 idempotency_key_not_supported. - 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
- A documented sandbox VIN completes the direct branch and stores distinct VIN and
tapiIdfields. - The redirect branch rejects a callback with wrong or reused
state, acceptsstatus=completed, and handlesstatus=cancelledwithout creating a vehicle mapping. - A
200parts result preservesprovider,matchLevel, category completeness, quantities, everytapiGenArtand every requiredvdiarray, including[]. - A sandbox
202parts job survives a process restart and reachessucceededwithout resubmitting the initiating call. - A unit test built from the documented production
failedjob example treats nestederror.codeas terminal even though polling returned HTTP200. - A completed redirect that began with a partial VIN can use its
tapiIdroutes but cannot enter parts or cart calls because the callback supplies no completed VIN. - A schema-valid but incomplete VIN-parts payload enters quarantine instead of creating an ERP parts list.
- Duplicate OE cart positions remain distinct and in order.
fits=falsepluscomplete=falseis rendered as unresolved, not incompatible.- Repeating a cart check with the same idempotency key produces no duplicate local operation and accepts
X-Tapinoma-Idempotent-Replay: true. - 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
fileUrlthat the caller is authorised to transmit and process. - A per-tenant API key and a data-minimisation decision for holder data.
quality=standardfor personal-document operations; no other processing level is currently offered by the contract.- Parsers generated from the pinned OpenAPI schemas
RegistrationDocumentV2Response,DocumentCalculationResponse,VinImageScanResponse, the 11-fieldDocumentExtractionResponseof the deprecatedextractDocumentpath and, when required,RegistrationDocumentEnvelopeResponsefor the national registration contract andVehicleDocumentExtractionResponsefor rich vehicle/equipment extraction; usePurchaseContractExtractionResponsefor purchase contracts.
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
- For an international vehicle registration document, call
POST /scanner/document/registration/international(extractInternationalRegistrationDocument) with{fileUrl, quality?}and anIdempotency-Key. New integrations should usefileUrl, not request-property or path aliases. UsePOST /scanner/document/registration(extractRegistrationDocument) only when the national{status,data}response contract is explicitly required. - Require HTTP
200andformat="tapinoma.vehicle-registration.v2". Persist only the policy-approved subset of thedocument,registration,holder,inspection,vehicle,fieldsandwarningsblocks as one versioned result. - Index
fields[]using the canonicalcode. KeepsourceCode, canonical Englishname, normalizedvalueandsourceValuetogether. Do not replace proper names, addresses or identifiers with translated guesses. - For a repair calculation or appraisal, or whenever calculation totals, a repair assessment or spare-parts, labour or paint line items are required, call
POST /scanner/document/calculation(extractCalculationDocument) with{fileUrl, quality?}and a separateIdempotency-Key. Its fixedDocumentCalculationResponsehas 12 required top-level fields:documentType,documentSubType,source,document,vehicle,financials,repairAssessment,parts,labor,paint,notesandequipment. There is no request field for a caller-defined output schema. Preserveequipment[].codes[].valueas a string together withequipmentKind,sourceSectionandsourcePage. - 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?}. KeepincludeSensitiveData=falseunless the caller has an approved, purpose-bound need for the content it withholds: without opt-in,sensitiveData.personsandsensitiveData.bankAccountsare empty, the scalar values ofsensitiveData.contractarenull, its amount objects contain onlynull, itsfieldslist is empty, anddocument.title,document.numberanddocument.datearenull. ValidateschemaVersion="1.0"and persist only the approved subset ofdocument,vehicle,equipment,sensitiveDataandwarnings. - For a vehicle purchase contract, call
POST /scanner/document/purchase-contract(extractPurchaseContract) with{fileUrl, quality?}. Use an authorised HTTPS URL without credentials for PDF, JPEG, PNG or WebP. PDFs may contain at most 14,000,000 bytes and images at most 7,000,000 bytes. Do not sendIdempotency-Key: this operation rejects it with HTTP400idempotency_key_not_supported. ValidateschemaVersion="1.0"and the closeddocument,seller,buyer,vehicle,purchase,handover,condition,signaturesandwarningsfields. Keep only the approved personal-data subset. All responses carryCache-Control: no-store. Signature presence and contractual declarations describe the source; they do not verify authenticity or legal validity. - The four old paths are deprecated compatibility aliases and never HTTP redirects:
/scanner/document/extract,/scanner/vehicle-document/extract,/scanner/registration-documentand/scanner/registration-document/v2. Follow the documented response and idempotency behaviour for each path. - 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 returnedvinis non-null and has all 17 characters. - 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
documentTypeanddocumentSubTypefromextractCalculationDocumentor the deprecatedextractDocumentpath as nullable strings without a controlled vocabulary, not enums;nullmeans the classification does not follow from the printed document. - In
extractVehicleDocument, validateequipmentKindandavailabilityindependently. Preservestandard,variantandspecialas 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 HTTP409idempotency_key_conflict. Do not send an idempotency key whenextractVehicleDocument.includeSensitiveData=true; the endpoint rejects it with400 idempotency_key_not_supportedand 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
- A sandbox international-registration call validates against its named OpenAPI response and maps every
fields[]element without losingsourceCodeorsourceValue. - A missing optional value remains null; the adapter does not populate it from another field.
- Calculation extraction maps empty
parts,labor,paintandnotesarrays without converting them to null and always returns the twelfth top-levelequipmentfield. - A multi-page calculation example preserves every documented equipment entry, keeps
codes[].valueas a string including leading zeroes, and does not mix entries from another section intoequipment. - Rich vehicle-document extraction keeps leading-zero equipment codes as strings and never infers
availabilityfromequipmentKind. - Vehicle-document extraction with
includeSensitiveData=truesends no idempotency key and persists no replayable raw response. - 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.
- 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. - A VIN-image result with
vin=nullcreates a review outcome and does not start a vehicle lookup. - 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.
- The same document ID in two tenants produces isolated calls with two different Client keys.
- 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, marketplace article content that must be independently checked before publication — 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-supportedmarketplaceIdenum value. The documented marketplaces are the eBay sitesEBAY_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_SGandEBAY_US, withEBAY_DEas the default. SEO languages arede,en,fr,es,it,nl,plandzh; 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
- Call
GET /parts/oe/normalize?oeNumber={raw}&manufacturer={optional}(normalizeOeNumber). Continue automatically only whenstatus=matchedandnormalizedOeNumberis non-null. The closed200always carriesversion,input,manufacturer,status,valid,reasons,lookupKey,normalizedOeNumber,matchRule,confidence,equivalentOeNumbers,equivalentOeNumberCount,equivalentOeNumbersTruncatedandknownCandidates; retain the permitted subset of these fields even when the outcome is not matched. - Call
GET /parts/oe/{oeNumber}(getOePart) with the confirmednormalizedOeNumber. Carry forward responsenormalizedOeNumber,tapiGenArt,vdi,part,fitment,replacementChain,referencesandreferenceNumbers. Preservepart.nameexactly as returned, includingnull. TreattapiGenArtandvdias independent best-effort enrichments: acceptnulland[]without discarding the confirmed base part or the other classification. KeepvehicleTypeKeyopaque and preserve every fitment entry's criteria with it.fitmentcontains the available type-level assignments and can be empty. Preserve each{manufacturer, numbers}reference group and the exact closedreferenceNumbers.oe_oem_reference_numbers[]list. Read relationship direction only fromreplacementChain; do not derive either reference view from that chain or the chain from those views. - For an indicative market range, call
GET /parts/oe/{oeNumber}/price(getOePrice) with explicitcondition, ISO 4217currencyandvehicleType. Persist the returned parameters,result,new,usedandpriceRecommendationtogether. - For the marketplace article itself, call
GET /parts/oe/{oeNumber}/seo(getPartSeo) with the configuredmarketplaceId,languageandvehicleType, and map the response onto the listing form:content.ebayTitleonto the eBay listing headline within its 80-character limit,categoryIdonto the marketplace category of the requestedmarketplaceId,itemSpecifics[]onto the article's item-specific fields as{name, value[]},keywords[]onto search terms,productonto product naming, andcontent.title,content.h1,content.metaTitle,content.metaDescription,content.slugandcontent.bulletPointsonto the shop page. Persist the returned publication fields with the generated text.404 seo_no_exact_matchis a documented no-match answer, not a failure of the item flow. Its closed body carries the requestedoeNumberin normalized form together withmarketplaceId,languageandvehicleType; record the no-match against that combination. - If a standalone part designation needs translation, call
GET /translation/translations?sourceLanguage={designation-language}&text={one-part-designation}(translatePartName). Both parameters are required:sourceLanguageis the language of the submitted designation, not a target language (the schema allows at most 20 characters, while the contract'ssource_language_too_longexample asks for a two-letter language code such asde), andtext(at most 1000 characters) must contain exactly one individual part designation, not a title, heading, product or offer title, description, sentence, list or several designations. A200always contains the four stringsen,fr,esandit; the operation has no target-language parameter. - For synthetic publication images, call
POST /vision/part/generate(generatePartImagesFromOeNumber) with anIdempotency-KeyandVisionPartGenerationRequest:oeNumberis required.anglescontains one to six unique values fromfront,rear,left,right,left_45andright_45; when it is omitted, all six are requested.surfaceFinish=manufacturer_colorrequirescolorCode; supplyingcolorCodewithautoselects that finish.referenceImageUrlscontains at most eight authorised URLs.sourceModeis one ofreferences_required(the default),references_preferredorknowledge_only;knowledge_onlymust not be combined with reference URLs.generateBaseModeldefaults totrue. Set it explicitly tofalseunless the shop needs the optional model asset and has a storage policy for it.
- The generation call returns
202. Retain the job reference and pollGET /vision/part/generation-jobs/{jobId}(getPartImageGenerationJob) whilestatusisqueuedorrunning. Onfailed, keeperror.codeanderror.message, both required inside the optionalerrorobject. Onsucceeded, validate every requiredVisionPartGenerationResultblock:format,oeNumber,coverage,basis,synthetic,disclosure,finish,part,dimensions,materials,paintableSurface,baseModel,views,identifierProtection,limitationsandgeneratedAt. Copy every requiredviews[].imageUrlinto the customer workflow and verifyviews[].sha256. WhenbaseModel.status=generatedandbaseModel.modelUrlis non-null, process that asset too and verifybaseModel.sha256when the field is present. - For a vehicle listing, only when the Client already owns a valid
tapiId, callPOST /vehicles/{tapiId}/listing(composeVehicleListing) with{quality?, language?, notes?}and anIdempotency-Key.languageisde,enorfr. Persistformat,tapiId,language,title,description,highlights,documentedEquipmentCountandtruncated.
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,ambiguousorinvalidnormalization.valid=truealone proves syntax, not existence. - A base-part
404or ambiguous422blocks 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. UsePOST /vin/cart-checkfor a specific VIN. - Validate
references[]as closed{manufacturer, numbers}groups andreferenceNumbersas the closed{oe_oem_reference_numbers}object. Do not infer direction, interchangeability or fitment from either reference view, and do not derive either view fromreplacementChain. - Read a zero price range through
result:no_listings_foundmeans no listing was usable, not that the part is worth nothing. A range is only evidenced whenresultispriced. If the evaluation could not be carried out at all, the endpoint answers with an error status rather than zeros. Thecurrencyquery parameter (defaultEUR) is documented as the ISO 4217 currency code for the monetary values in the response, and every200carries a requiredcurrencyfield. - A successful SEO response is publication content, never a fitment statement, and must be independently checked for accuracy and permissibility before publication. Keep the optional
liveAvailabilityobject with the stored result when it is present.429 seo_live_requests_pausedis retryable and carries a requiredliveAvailabilityobject (status=PAUSED,reason=RATE_LIMIT_REACHED,availableAt,retryAfterSecondsfrom 1 to 86400); theX-Tapinoma-Live-Available-Atheader states the earliest retry. - 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
tapiIdscope 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-group, consolidated reference-number 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 basis and disclosure fields (basis, synthetic, disclosure, coverage, limitations and each view's partIdentityVerified and verificationConfidence), 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
matchednormalization proceeds with the returnednormalizedOeNumber; all other statuses stop before the base-part call.- The adapter accepts both populated and empty
fitmentlists, keeps everycriteria[]value with itsvehicleTypeKeyand never manufactures assignments locally. - 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. - The price adapter preserves
condition,currency,vehicleType,resultand confidence with the range. - The SEO adapter accepts the documented publication fields, fills the listing title, category and item specifics from
content.ebayTitle,categoryIdanditemSpecifics[], tolerates anullcategoryIdand an emptyitemSpecifics[], and handles optional live-availability metadata separately. - Image generation persists
202before polling, resumes after restart and verifies each downloaded asset againstsha256. - A generated base-model asset is copied to controlled storage and its checksum is verified when present;
generateBaseModel=falsecreates no dependency onmodelUrl. knowledge_onlyplusreferenceImageUrlsis rejected before transmission.- The published image is visibly governed by the returned synthetic disclosure and never labelled as a stock photograph.
- 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
vehicleTypefor 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
- Call
POST /scanner/label/extract-partnumbers(extractLabelPartNumbers) with{imageUrl, quality?}and anIdempotency-Key. - Read
partNumbers[]; production and sandbox use the same response shape. Preserve every candidate and do not select one only because it appears first. - For each candidate, call
GET /parts/oe/normalize(normalizeOeNumber) withoeNumberand an optional manufacturer hint. Proceed only with a confirmedmatchedresult. - Call
GET /parts/oe/{oeNumber}(getOePart) for the confirmed normalized number. StoretapiGenArt,vdi[],part,fitment,replacementChain,referencesandreferenceNumbersas distinct reference data linked to the physical stock unit. Preservepart.nameas the returned string ornull. TreattapiGenArt: nullandvdi: []as independent missing enrichments, not as a failed base lookup. Keepcriteria[]attached to its opaquevehicleTypeKey.fitmentcontains 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. - Load the classification catalogue with
GET /vdi(getVdiCatalog). Prefer the unpaginated response when feasible. If paging is required, uselimitfrom 1 to 500, advanceoffsetbypage.returned, and stop onpage.hasMore=false. Version the local index bystand.catalogVersion. This catalogue does not return part mappings; use only VDI codes already returned bygetOePartor by a successful VIN-parts position for the link. - Optionally call
GET /parts/oe/{oeNumber}/aftermarket-references(getOeAftermarketReferences). These are reference records, not interchange or fitment guarantees. Prefer an unpaginated response; if paging, usepage.returnedandpage.hasMore. - Optionally call
GET /parts/oe/{oeNumber}/price(getOePrice) with explicitcondition,currencyandvehicleType. - For cluster-level market evidence, call
GET /parts/intelligence(resolvePartIntelligence) with the confirmed OE number, explicit market dimensions and its requiredIdempotency-Key. This is a pilot operation: the paid production pilot is commercially closed and delivers no real results without approved positive prices and atomic billing. Without a complete price configuration for the four initiating Intelligence operations, every Intelligence route, including both status polls, answers503 market_intelligence_billing_unavailableregardless of the API key. On202, persist and poll onlyGET /parts/intelligence/jobs/{jobId}(getPartIntelligenceJob). On completion, keepcluster.clusterId,cluster.clusterVersion,scope,asOf,validUntilandprovenance.snapshotIdtogether. - With a known cluster, read current Intelligence through
GET /parts/clusters/{clusterId}/intelligence(getPartClusterIntelligence) or materialized snapshots throughGET /parts/clusters/{clusterId}/history(getPartClusterIntelligenceHistory). Both require their own idempotency key; history also requiresfromandtoand never starts a job. - For recall context, batch one to 100 stock positions into
POST /recalls/parts(matchPartRecalls). Send each as{oeNumber, reference, vehicle?}wherereferenceis the local stock-unit key of at most 200 characters, returned unchanged, and the optionalvehiclecarriesmake,model,productionFromandproductionTo; use anIdempotency-Key. The response preserves position order. Map results by returnedreferenceand keep each position'sstatus(matched,no_measuresorunresolved),unresolvedReasonandmeasures[]withregisters[].dataStandand the trilingualnotice.404 recall_positions_unresolvablemeans that no position could be assigned to a manufacturer.
Vehicle-level dismantling evaluation
- Call
GET /vin/{vin}/economic-evaluation(getVinEconomicEvaluation) with a known full VIN and the documented business parameters such ascountry,condition,vehicleType,maxPricedParts,recoveryRateandcostPerPart. - On
200, persist the result. On202, persistjobId,statusUrlandcreatedAtand pollGET /vin/economic-evaluation/jobs/{jobId}(getVinEconomicEvaluationJob), observingRetry-Afterwhilestatusisqueuedorrunning. Onsucceeded,resultcontains the complete evaluation; onfailed, theerrorobject carriescodevehicle_not_found(no-result outcome),vin_service_unavailable(technical failure), ormarket_pricing_unavailable(no usable market prices for the evaluation). - Consume the echoed
assumptions,coverage,revenuePotential,purchaseRecommendationand rankedpartstogether. PreservewarningsandliveAvailabilitywhen present; both are optional. Incomplete coverage makes the totals a lower bound. - If the workflow needs the underlying vehicle-related parts without an economic model, call
GET /vin/{vin}/parts(getPartsByVin) and follow its200/202branch viaGET /vin/parts/jobs/{jobId}(getVinPartsJob). RetainmatchLevelandmissingCategories. - When cluster-based valuation is required, call
POST /vin/economic-intelligence(createVinEconomicIntelligence) with the body fieldsvin,marketCountry,currencyandconditionand a separateIdempotency-Key. The route is deployed in the production pilot but remains disabled until the single-dispatch and billing reconciliation gate has been passed; while disabled it answers503withmarket_intelligence_billing_unavailableormarket_intelligence_vin_not_enabled. On202, poll onlyGET /vin/economic-intelligence/jobs/{jobId}(getVinEconomicIntelligenceJob). Keep finalevaluationId,pipeline,portfolio,candidates,confidence,provenanceandwarningstogether; do not substitute this response for thegetVinEconomicEvaluationresponse 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 []. Catalogue code values match ^VDI4081_[0-9]+$, while mapping values match ^VDI4081_[0-9]+(?:_[0-9]+)?$, so a mapping value is not always identical to a catalogue code of the separately versioned 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
ambiguousandunresolvedas review states. Do not join onlookupKeyas 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
countand paginated list length, do not usecount == aftermarketReferences.lengthas a paging invariant. Usepage.returnedandpage.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
warningsthat 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 and
POST /vin/economic-intelligencerequire the header with 16 to 100 characters matching^[A-Za-z0-9_-]+$;extractLabelPartNumbersandmatchPartRecallsaccept an optional key of 8 to 255 characters matching^[A-Za-z0-9._:-]+$. The two Intelligence status polls do not accept the header. Serialize calls per tenant Client, including polls. - Keep
observedSupply,verifiedSalesandinternalUsageseparate. Supply listings are not sales. Block automated pricing, dismantling or acquisition whenpublishable=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 candidates from partNumbers[], 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
- The adapter reads the single declared
partNumbers[]property of the closed label response, accepts an empty list as “no part number recognised” and preserves each candidate as a separate value. - Two OCR candidates remain separate until each normalization result is evaluated.
- A VDI catalogue refresh is keyed by
catalogVersion; an OE mapping is never inferred from catalogue content. - Unpaginated and paginated classification loads produce the same entry set; pagination advances
offsetbypage.returnedand stops onpage.hasMore=false. - Recall batch responses map back to duplicate OE numbers by unique local
reference, not by number alone. - A
202economic job resumes after restart, and a nested failed status never creates a recommendation. - A response with incomplete coverage is labelled as a lower bound and cannot pass an automatic purchase threshold without the configured review rule.
- Two tenants using the same stock-unit string remain isolated by Client key.
- A resolver
202survives restart and polls only the part-Intelligence status route; itsjobIdfails safely on the VIN status route. - History never creates a job or a synthetic missing point, and cluster version, snapshot ID, scope and
asOfremain attached to every stored point. - Observed-supply counts never populate verified-sale counts, and
publishable=falsecannot trigger automated pricing or acquisition. - Final VIN polling yields an
evaluationIdequal to the precedingjobIdandrequestIdin 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
fileUrland, optionally, one to five authorisedphotoUrlsof the same vehicle. - Optionally a two-letter
country(defaultde; a value that is not a two-letter ISO 3166-1 code returns400 invalid_country) and an explicit decision whether holder data is necessary.includeOwnerdefaults tofalse; keep that default unless the intake purpose requires holder data. - 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
- Call
POST /vehicles/intake(intakeVehicle) with{fileUrl, photoUrls?, country?, includeOwner?}and anIdempotency-Key. - A successful intake is synchronous HTTP
200; this operation has no202branch. Validateformat,registrationDocument,vin,provider,tapiId,vehicle,conditionReport,componentsandcomplete. - Evaluate each component independently:
components.registrationDocumentisdeliveredon every successful response.components.vehicleisdelivered,vin_not_readableorunavailable.components.conditionReportisdelivered,failedornot_requested.complete=trueonly when every requested component was delivered.
- If
tapiIdis non-null, optionally callGET /vehicles/{tapiId}(getVehicleByTapiId) to retrieve the technical vehicle-type record. - If a new or larger photo set must be assessed independently, call
POST /vision/condition-report(reportVehicleCondition) with one to eightimageUrlsof the same vehicle, optionalquality(standardby default,enhancedormaximum) and a newIdempotency-Key. Preserve all nine required fields:format,gradable,grade(A,B,Cornull),reason,summary,zones(each offront,flanke_links,flanke_rechts,heck,dach,innenraum,motorraumandsonstigesexactly once and in this order, each with itsfindingsarray),limitations,imageCountandvisualOnly. - If
vinis non-null, optionally callGET /recalls/vehicles/{vin}(matchVehicleRecalls) without anIdempotency-Keyand preservevin,measures,measureCount,registers[](register,dataStand,measureCount) and thenoticeobject with itsde,enandfrtexts. A VIN that cannot be resolved returns404 vin_not_resolvable. - If
tapiIdis non-null and publication text is required, callPOST /vehicles/{tapiId}/listing(composeVehicleListing) with the optional body{quality?, language?, notes?}and a separateIdempotency-Key;qualityisstandard(default),enhancedormaximum,languageisde(default),enorfr, andnotesis at most 500 characters, of which only factual statements are used. A foreign or unknowntapiIdreturns404 vehicle_not_found.
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 the partial-success workflow and billing reconciliation under the agreed terms; 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. Unless
includeOwner=true, the returnedregistrationDocument.ownerisnull; keep it null locally and do not enrich it elsewhere. - A successful HTTP
200can still be a partial vehicle file. Never require non-nullvin,tapiId,vehicleorconditionReportwithout checkingcomponents. - The condition report rests on visual inspection alone;
visualOnlypermanently indicates that the report remains a description: no expert appraisal and no repair or residual-value calculation. A null grade withgradable=falseis a valid outcome; retainreasonandlimitations. - 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
tapiIdcontinuation. A404from 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
- A complete sandbox intake validates all required response fields and links the local intake ID, VIN and
tapiIdwithout conflating them. - A fixture with
components.vehicle=vin_not_readablepersists the document result, leaves vehicle fields nullable and does not call VIN-dependent endpoints. - A fixture with
components.conditionReport=failedrecords a partial success and does not discard delivered document or vehicle data. includeOwner=falsenever produces a locally populated owner record.- A condition report with
gradable=falseandgrade=nullenters review and preserves all limitations. - Replaying the intake key produces one local intake result and recognises the idempotent response headers.
- A
tapiIdobtained 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
externalReferencematching^[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
- Reserve the local
externalReferencein a pending provisioning record before any network call. - With the master key, call
POST /client/partner-workspaces(createPartnerWorkspace) with noIdempotency-Key. Send:- required
name,externalReferenceand one to 50 contract-approvedendpointKeys(administrative endpoints are not allowed); - optional
sponsorship(defaultfalse;truemeans the caller grants the workspace sponsorship for exactly the enabled endpoints in thehubchannel as part of the same request, otherwise the returnedsponsorshipGrantisnull) andapplicationLabel(label for the generated API key). - Omit
rateLimitsin this call: its schema types the items only asobject; the description refers to the same format asPOST /client/users(createClientUser), which types them asRateLimitInput, and to a cap by operator limits. Apply limits through the typed operation in step 4.
- required
- On
201, validate thatworkspace.idandworkspace.externalReferenceare present before activating the tenant; the top-levelworkspaceis 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 rawapiKey.key,allowlistandsponsorshipGrant. TheapiKeyobject'sid,label,keyandcreatedAtare required. Never write the raw key to logs or ordinary database columns. - 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 ofworkspace.id. Use the Master client key,{rateLimits}and anIdempotency-Key. EachRateLimitInputsuppliesendpointKey,windowSeconds,maxRequests, optionalapiKeyIdand optionalisActive. - If sponsorship terms require a later explicit grant, call
PUT /client/sponsorship-grants/{grantReference}(upsertSponsorshipGrant) with the Master client key, a stablegrantReference, an explicitly sourcedbeneficiaryClientId, one to 50 unique approvedendpointKeysand, where the agreed terms require them, the optionalchannels(onlyhub),startsAt, exclusiveendsAt,partnerTermsAllowedandbeneficiaryMonthlyCapCents. OmittingpartnerTermsAllowedorbeneficiaryMonthlyCapCentsleaves the existing setting unchanged; settingpartnerTermsAllowedback tofalsedrops a billing-mode choice already made. Use anIdempotency-Key; do not inferbeneficiaryClientIdfromworkspace.id. - To set the beneficiary's billing terms, use its own tenant key for
PUT /client/sponsorship-grants/received/{grantReference}/billing-mode(chooseSponsorshipBillingMode) with{mode, sponsorClientId?}and anIdempotency-Key.modeisownorpartner;partnerrequires the sponsor'spartnerTermsAllowedrelease, otherwise the operation returns403. SendsponsorClientIdwhen several sponsors use the samegrantReferencefor this beneficiary; without it that case returns409 ambiguous_grant_reference.
Runtime routing
- Authenticate the middleware caller and resolve exactly one local tenant record.
- Reject any requested capability outside that Tenant's local allowlist before selecting a tapinomahub path.
- 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.
- 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.
- Apply the shared status/error policy. Persist a
202before releasing the Client queue; schedule the documented poll through the same per-Client queue and matching tenant key. - 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'sclientId,billingMode(ownorpartner), current balance (balanceCents,balance,currency), overdraft allowance (creditLimitCents) and available amount (availableCents); underpartner,balanceCents,balanceandcurrencyare null andcreditLimitCentsandavailableCentsare null or absent.
Key and Client lifecycle
- 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 sendIdempotency-Key. Before cutover, requireapiKey.id,apiKey.label,apiKey.keyandapiKey.createdAt; neither the response wrapper norApiKeyCreateResponsecurrently marks these fields as required. If any is missing, enterreconciliation_requiredand do not repeat the call automatically. Store a complete raw key once as for initial provisioning. - Move traffic to the new secret only after a successful authentication test such as
GET /ping(getSystemStatus). - Deactivate a Tenant with
PATCH /client/users/{clientId}(updateClientUser) only for an explicitly established Client ID and{isActive:false}using anIdempotency-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, unique within the Master client; a duplicate submission returns 409 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_requiredand do not call again automatically. A duplicateexternalReferenceprevents a second partner workspace, but the public contract has no lookup-by-external-reference or raw-key recovery operation. RateLimitInputrequiresendpointKey,windowSecondsandmaxRequestsin 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
- Provisioning returns one Client and stores the one-time raw key only in the secret manager; logs and database exports contain no secret.
- A workspace success missing
workspace.idorworkspace.externalReferenceand an additional-key success missing raw key material both enter reconciliation rather than activation. - Provisioning omits untyped
rateLimits; the subsequent typed replacement call persists the returned limit set. - An uncertain provisioning response enters reconciliation and is not automatically repeated.
- A caller cannot override its resolved Client key or invoke an operation outside the local allowlist.
- Two tenants run in parallel, while two requests for one Client execute serially.
- A
202job remains tenant-partitioned, survives restart and polls with the same Client key. - Every operation declaring idempotency has a durable pre-dispatch record; Intelligence data GETs carry the header and GET status polls do not.
409 idempotency_key_conflict,409 idempotency_request_in_progressand409 idempotency_result_unavailablereach distinct middleware states.- 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. - 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.