Runtime, errors and security
This page defines the integration control plane shared by every capability. Exact operation-specific headers, schemas, status codes and error examples remain in OpenAPI.
Base URL and authentication
- Base URL:
https://api.tapinomahub.com - Default authentication scheme: API key in the
X-Api-Keyrequest header. It applies to every documented operation, includingGET /ping. - The six Intelligence operations additionally accept the verified first-party session cookie
__Host-tapinomahub_sessionas an alternative for an explicitly enabled pilot account. Server integrations should useX-Api-Key.POST /vin/economic-intelligencedeclares the optionalX-CSRF-Tokenheader: it is required when that request is authenticated by the session cookie and not required withX-Api-Key; an invalid CSRF proof returns403 invalid_csrf. - Production and sandbox use the same base URL; the key selects the environment.
Keep API keys in a server-side secret store. Never place them in a browser bundle, mobile-app binary, redirect URL, query parameter, log, analytics event, screenshot, support ticket or documentation console.
Use a separate Client and key when customer, application, quota, usage or billing isolation is required. A Tenant in the customer system is not an API field.
Minimal client policy
Every production adapter should have one shared response wrapper that:
- records the operation ID, HTTP method and path template without recording secrets;
- captures HTTP status and the documented operational headers;
- parses JSON only when the response media type and operation declare it;
- on non-2xx, branches on the stable top-level
errorcode and never onmessage; - on a job-status
200, branches again on the job's domainstatus; - rejects a payload that does not satisfy the pinned OpenAPI schema;
- returns a typed domain result to the calling ERP, DMS, shop or worker.
Do not build each endpoint as an unrelated HTTP call. Authentication, retry, idempotency, async state, tenant isolation and observability must be consistent across the adapter.
Response decision matrix
| HTTP outcome | Meaning | Client action |
|---|---|---|
2xx other than 202 |
Transport-level success | Validate the operation-specific success schema. For status resources, still inspect the embedded domain state. |
202 |
Work accepted, result not final | Validate and persist the workflow-specific full statusUrl, jobId, Retry-After, input correlation and next-poll time. Capture Location only for comparison: on GET /vin/{vin}/parts, POST /vin/cart-check and GET /vin/{vin}/economic-evaluation it is a uri-reference and can be relative, so resolve it against the request URL before comparing; quarantine a mismatch. Poll statusUrl; do not resubmit the order. |
400 |
Request is invalid or operation-specific processing rejected it as documented | Branch on error. Correct the request or route it to review; retry only when the global error catalogue explicitly marks that code retryable. |
401 |
missing_api_key: the X-Api-Key header is missing. On the six Intelligence operations authentication_required: the session or API key is missing, invalid or cannot be assigned unambiguously to a pilot account |
Fix secret injection or the pilot-account assignment. Do not retry unchanged in a loop. |
402 |
Billing or sponsorship cannot cover the call | Stop the business operation and resolve funding/terms. |
403 |
invalid_api_key (key unknown, deactivated or blocked), endpoint_not_allowed (endpoint not open to this Client), forbidden, sandbox_endpoint_unavailable (endpoint not available in the sandbox) or, on PUT /client/users/{clientId}/rate-limits, rate_limit_above_operator_default. The six Intelligence operations use market_intelligence_not_enabled, email_not_verified, account_locked and, on POST /vin/economic-intelligence, invalid_csrf |
Stop and resolve access or correct the request; do not convert it to “not found”. |
404 |
Selected resource or documented business match not found | Handle as an operation-specific negative outcome. Do not try a fabricated alternate path. |
409 |
Request-state or idempotency conflict | Branch on error; only the explicitly retryable in-progress condition may be repeated unchanged after Retry-After. |
413 / 415 |
Documented only for POST /vin/economic-intelligence: 413 request_body_too_large when the JSON body exceeds the pilot limit of 32 KiB, 415 unsupported_media_type when the body is not sent as application/json |
Reduce or correct the request body before a new request. |
422 |
Only where the operation declares it: ambiguous or invalid input (for example ambiguous_oe_number, ambiguous_cluster, invalid_identifier), a required evidence tier that is not reached (insufficient_evidence), an image edit that could not be carried out or verified (part_segmentation_failed, damage_not_transferable, identifier_redaction_unverified, image_edit_unverified), or an unavailable or failed Intelligence refresh or job (refresh_unavailable, refresh_failed, refresh_timeout, refresh_completed_without_snapshot, refresh_job_missing, intelligence_job_failed, vin_intelligence_failed) |
Branch on error; route to review or request better input; do not guess a candidate. No 422 code is marked retryable. |
429 |
rate_limit_exceeded (rate limit reached), client_request_in_progress (another request of the same Client is still active; not documented on the six Intelligence operations) or, on GET /parts/oe/{oeNumber}/seo, seo_live_requests_paused (live enrichment paused; X-Tapinoma-Live-Available-At states the earliest retry) |
Read error and Retry-After; serialize per Client and delay retry. |
5xx |
Temporary API failure | Consult the global error catalogue's retryable flag. Preserve the same idempotency key for an ambiguous write. |
The same error code can require evaluation together with HTTP status. Apply only the retry behaviour documented for the selected operation and response status.
In a successful GET /parts/oe/{oeNumber} response, the required part.name is a non-empty string when a part designation is available and null otherwise. Preserve the returned value, and do not reconstruct a missing name from another field. The required fitment array lists the available vehicle-type assignments and can be empty; an empty list makes no statement about undocumented vehicles.
Error object
The common non-2xx body is:
{
"error": "stable_machine_code",
"message": "Human-readable explanation"
}
Only error is a program key. message is explanatory text for logs and support; branch on error only. On the six Intelligence operations only error is required: message and requestId (UUID) are optional, and their 4xx responses can add a details object. GET /parts/oe/{oeNumber}/seo adds oeNumber, marketplaceId, language and vehicleType to 404 seo_no_exact_match and liveAvailability to 429 seo_live_requests_paused. The status operations of the VIN parts, cart-check and economic-evaluation jobs and of image generation report a failed job inside the 200 body with status failed; their embedded error object carries code and message instead of a string error, so branch on error.code there. The OpenAPI contract documents retry behaviour and operation-specific schemas and examples determine which codes are evidenced on each status; extra response fields remain governed by that response schema.
Do not assume that the global catalogue alone proves that every error applies to every operation. Use only errors documented for the selected operation and status.
Asynchronous jobs
9 public operations can return 202; they form 6 documented job families. The two Intelligence status operations themselves return 202 while work remains pending and therefore count as operations with a 202, not as additional job families.
The two Intelligence job families are conditional pilot contracts. Intelligence billing, adaptive part refresh and VIN Economic Intelligence are currently disabled in production. While Intelligence billing is not enabled, all six Intelligence operations answer 503 market_intelligence_billing_unavailable before authentication, so none of the following branches is reached. While refresh is disabled, GET /parts/intelligence and GET /parts/clusters/{clusterId}/intelligence return 422 refresh_unavailable where they would otherwise accept a refresh with 202. While VIN Economic Intelligence is disabled, POST /vin/economic-intelligence and GET /vin/economic-intelligence/jobs/{jobId} both return 503 market_intelligence_vin_not_enabled, also for a job accepted earlier. Apply the part-refresh polling row to a job that was already accepted or after refresh has been enabled, and the VIN Economic Intelligence row only after that feature has been enabled.
| Order operation | Status operation | Important distinction |
|---|---|---|
GET /vin/{vin}/parts |
GET /vin/parts/jobs/{jobId} |
The order is a GET but may still return 202; do not create a second job by changing lookup context. |
POST /vin/cart-check |
GET /vin/cart-check/jobs/{jobId} |
Use the returned jobId and statusUrl only with this status operation. |
GET /vin/{vin}/economic-evaluation |
GET /vin/economic-evaluation/jobs/{jobId} |
Preserve the input assumptions used for the evaluation. |
POST /vision/part/generate |
GET /vision/part/generation-jobs/{jobId} |
Preserve synthetic/disclosure metadata on the resulting assets. |
GET /parts/intelligence or GET /parts/clusters/{clusterId}/intelligence |
GET /parts/intelligence/jobs/{jobId} |
Both orders share the part-refresh status family. Preserve the resolved cluster, version, market scope and original idempotency key; never poll the history route. |
POST /vin/economic-intelligence |
GET /vin/economic-intelligence/jobs/{jobId} |
Preserve the VIN request dimensions. In the current pilot, accepted requestId and jobId are the same UUID and the final result repeats it as requestId and evaluationId. |
GET /parts/clusters/{clusterId}/history has no asynchronous branch and never creates missing history.
Generic polling flow:
send order once
-> final 2xx: validate and complete
-> 202:
validate the required absolute statusUrl against the documented status operation
resolve a relative Location against the request URL, compare it with statusUrl; quarantine a mismatch
persist workflow family + jobId + nextPollAt
wait at least Retry-After seconds
GET statusUrl
-> HTTP 202 queued/running where declared: update nextPollAt and wait
-> HTTP 200 on the VIN parts, cart-check, economic-evaluation or image-generation status operation: inspect status
queued/running: update nextPollAt and wait
succeeded: require the workflow's usable result, or quarantine
failed: map error.code of the embedded error object, or quarantine
-> HTTP 200 on an Intelligence status operation: final result; validate it
-> HTTP 422 on an Intelligence status operation: job ended without a usable result; branch on error
Polling the six documented status families belongs to the original order and does not create a second order. The current Intelligence pilot remains closed until approved positive pricing and atomic billing are enabled together; no real result is released before then. Test every documented HTTP and domain-status branch against its schema.
Do not infer lifecycle behaviour that the selected operation does not document.
Idempotency
The support signal is the operation's declared Idempotency-Key header parameter. Do not infer support merely because the method is POST, PUT, PATCH or DELETE. Every operation that does not declare the header, except the two Intelligence status operations, documents 400 idempotency_key_not_supported for a request that sends it.
For an operation that declares the header:
- Generate one opaque key for one logical business action that satisfies the operation's declared schema: 8 to 255 characters matching
^[A-Za-z0-9._:-]+$where the header is optional, 16 to 100 characters matching^[A-Za-z0-9_-]+$on the four Intelligence operations that require it. A key outside that schema returns400 invalid_idempotency_key. - Persist it before sending the request.
- Reuse the same key after a timeout or other ambiguous transport failure.
- Never reuse it for a different request.
- Keep the key scoped to the Client and business action in local storage.
The three Intelligence data GET operations GET /parts/intelligence, GET /parts/clusters/{clusterId}/intelligence and GET /parts/clusters/{clusterId}/history are explicit exceptions to the usual read convention: like POST /vin/economic-intelligence, each requires Idempotency-Key. The two Intelligence status operations GET /parts/intelligence/jobs/{jobId} and GET /vin/economic-intelligence/jobs/{jobId} accept only the path parameter jobId and do not declare that header. Follow the selected operation rather than deriving idempotency from its HTTP method.
The central replay details below apply only where the selected operation declares the corresponding response headers and error codes. The four Intelligence operations that require the key (the three data GETs and POST /vin/economic-intelligence) document X-Tapinoma-Idempotent-Replay: true and 409 idempotency_key_conflict (key already used for a different business scope), but not X-Tapinoma-Idempotency-Stored, idempotency_request_in_progress, idempotency_result_unavailable or a 24-hour retention promise. Do not infer those central behaviours for an Intelligence request.
Documented behaviour:
- A completed repeat returns the earlier response without re-execution or a second charge and sets
X-Tapinoma-Idempotent-Replay: true. - The first successful response reports
X-Tapinoma-Idempotency-Stored: true|false. falsemeans the same key returns409 idempotency_result_unavailable; do not switch automatically to a new key.- While the first call is active, the same key can return
409 idempotency_request_in_progress. Repeat the unchanged request afterRetry-After. - Earlier successful results remain retrievable with the same key for 24 hours.
- For a failed call, follow the documented error code, refund rule and retry guidance.
- The key space is per Client. The same key with a different request returns
409 idempotency_key_conflict.
The renamed scanner paths use compatibility aliases rather than HTTP redirects. Follow the documented request, response and idempotency behaviour for each alias pair:
/scanner/document/registrationand deprecated/scanner/registration-document;/scanner/document/registration/internationaland deprecated/scanner/registration-document/v2;/scanner/document/vehicleand deprecated/scanner/vehicle-document/extract.
For these three pairs, an identical body and key may be retried through either path and returns the same stored result without re-execution or a second charge. Exception: /scanner/document/vehicle and its deprecated alias also document 400 idempotency_key_not_supported, and the contract excludes the processing of sensitive document data from idempotency; do not send Idempotency-Key together with includeSensitiveData: true. The purchase-contract scanner (POST /scanner/document/purchase-contract) always rejects Idempotency-Key with 400 idempotency_key_not_supported and returns Cache-Control: no-store on every documented response. After a connection loss or ambiguous outcome, check the request outcome before submitting a new paid operation; do not assume that repeating the URL is an idempotent retry. Calculation extraction is deliberately not an identical response pair: /scanner/document/calculation returns 12 top-level fields including equipment, while deprecated /scanner/document/extract retains the 11-field response without it. Reusing a key across those two response contracts returns HTTP 409 (idempotency_key_conflict); repeat an ambiguous request only on the same path contract.
The three key-issuing operations deliberately do not support idempotency:
POST /client/usersPOST /client/users/{clientId}/keysPOST /client/partner-workspaces
Their success payload contains a new plaintext API key and cannot be repeated safely after an uncertain outcome. Enter an operation-specific reconciliation_required state and never repeat the write blindly. POST /client/users can use GET /client/users to determine whether a Client was created, but that list cannot recover the lost plaintext key. The public contract exposes no raw-key recovery for POST /client/users/{clientId}/keys and no partner-workspace lookup by externalReference. Because externalReference must be unique within the Master client, a repeated POST /client/partner-workspaces with the same reference returns 409 workspace_reference_exists; that confirms the workspace exists but does not return its API key. Recovering a lost plaintext key from either operation requires an operator-approved recovery procedure.
Retry policy
Use the global error catalogue, HTTP status and operation contract together.
if final success:
do not retry
else if 202:
poll the validated full statusUrl; do not resend order
else if response.error is not marked retryable:
stop or request corrected state/input
else if Retry-After exists:
wait at least that duration
else:
use bounded exponential backoff with jitter
for an ambiguous supported write:
retry with the same Idempotency-Key
for an unsupported key-issuing write:
enter operation-specific reconciliation_required
use published list state only where it exists
require operator-approved recovery when the public contract is insufficient
never repeat blindly
The exact maximum attempt count and time budget are customer-system policies; the API contract does not publish universal values. Keep the total below the calling system's business deadline and surface exhaustion as an operational state.
Rate limits and concurrency
Rate limits apply per endpoint_key. A Master client sets them for a sub-user with PUT /client/users/{clientId}/rate-limits; POST /client/users and POST /client/partner-workspaces accept the same optional rateLimits array. Each entry carries endpointKey (one endpoint key such as vin.vehicle, or * for a Client-wide limit across all endpoints), windowSeconds, maxRequests, optionally apiKeyId (null applies to all of the user's keys) and isActive (default true). PUT /client/users/{clientId}/rate-limits answers a windowSeconds value that is not an operator-defined window with 400 unsupported_rate_limit_window and a limit above the operator default with 403 rate_limit_above_operator_default. Once the counter is reached, the API answers 429 rate_limit_exceeded with Retry-After equal to the window length. Where no limit is configured, default limits apply; their numeric values are not published in the contract.
Independent of numeric rate limits, only one API request may be active per Client. Another concurrent request is rejected with 429 client_request_in_progress. This is not a contract-partner-wide cap: separate Clients can run in parallel. The six operations GET /parts/intelligence, GET /parts/clusters/{clusterId}/intelligence, GET /parts/clusters/{clusterId}/history, GET /parts/intelligence/jobs/{jobId}, POST /vin/economic-intelligence and GET /vin/economic-intelligence/jobs/{jobId} do not document this rule; their only 429 code is rate_limit_exceeded, which reports the shared tenant-wide per-minute limit across all API replicas.
Recommended queue shape:
queue key = clientId (or local Client mapping)
concurrency per queue = 1
cross-Client workers = bounded by the partner's own resources and agreed limits
429 -> read error + Retry-After -> reschedule, do not busy-loop
When effective numeric defaults are not published, do not hard-code guessed requests-per-minute values. Use only limits supplied through an approved contract or documented mechanism.
Pagination
The contract's long-list pattern uses:
limit: 1 to 500offset: number of entries to skip (integer, minimum0); allowed only together withlimitcount: total result countpage.returned: length of this pagepage.hasMore: whether another page follows
API 1.71.0 exposes this pattern on GET /parts/oe/{oeNumber}/aftermarket-references (list field aftermarketReferences) and GET /vdi (list field entries). page is not a required response field; when it is present, page.limit, page.offset, page.returned and page.hasMore are all required. Without limit, the contract says the complete result is returned. Every page is a separate call and is billed as a call, so a complete unpaginated read remains cheapest when its size is operationally acceptable.
Use page.hasMore, not a comparison of the returned list length with limit, as the continuation signal. Keep the same query semantics across pages and validate the selected operation's response schema.
Operational response headers
Read only headers declared by the selected operation. The global vocabulary includes:
| Header | Use |
|---|---|
Location |
Status URL of a job accepted for asynchronous processing, declared on the 202 response of the nine operations that can return 202. It is a uri-reference on GET /vin/{vin}/parts, POST /vin/cart-check and GET /vin/{vin}/economic-evaluation (the contract example is a relative path) and a uri on POST /vision/part/generate, GET /parts/intelligence, GET /parts/clusters/{clusterId}/intelligence, GET /parts/intelligence/jobs/{jobId}, POST /vin/economic-intelligence and GET /vin/economic-intelligence/jobs/{jobId}. |
Retry-After |
Minimum seconds before retry or the next poll. |
X-Tapinoma-Billing-Source |
Technical settlement decision: plan, balance, bundle, sandbox or idempotent_replay; the value alone proves neither a debit nor a product price. |
X-Tapinoma-Usage-Warning |
Human text at high or exhausted plan usage; do not parse it as a code. Multiple warnings can be joined with ` |
X-Tapinoma-Billing-Bundle |
Monthly bundle associated with the request. |
X-Tapinoma-Sponsorship-Grant |
Associated sponsorship reference; it does not prove this call caused a sponsor debit. |
X-Tapinoma-Live-Available-At |
Earliest time (date-time) at which temporarily paused live processing should be retried; declared by GET /parts/oe/{oeNumber}/seo and GET /parts/oe/{oeNumber}/price. |
X-Tapinoma-Idempotent-Replay |
Response came from a prior completed request. |
X-Tapinoma-Idempotency-Stored |
First-success replay-storage result. |
Header names are case-insensitive. Their values are not substitutes for validating the response body.
Billing-aware behaviour
- Concrete prices are not published in the API contract; billing and contract-included account or status operations are governed exclusively by the agreed terms.
- A plan is assigned per
endpoint_key, not globally to a Client. - Different endpoint keys can be covered by different active plans.
- If no active plan covers the endpoint, or its monthly quota is exhausted, billing falls back to the Client's credit balance.
- VIN Vehicle, VIN Parts and VIN Cart Check form the monthly bundle
VIN_MONTHLY_LOOKUP; billing and repetitions are governed exclusively by the agreed terms, and cache or result reuse does not change customer billing. - From 90% plan consumption, the API can emit
X-Tapinoma-Usage-Warning. - A pre-charged individual call that ends with HTTP status 400 or above is generally refunded automatically. Combined workflows and contract-specific conditions follow the rules in the relevant service description or agreement. Code-specific billing statements are in the global error catalogue for
billing_unavailable,endpoint_not_priced,idempotency_request_in_progress,internal_error,partner_terms_not_covered,recall_positions_unresolvable,sponsorship_cap_reachedandvin_not_resolvable, in the404 seo_no_exact_matchschema ofGET /parts/oe/{oeNumber}/seo, and in the cart-check and economic-evaluation status operations for afailedjob.
Do not calculate customer invoices from a single response header. Reconcile usage and balance with GET /client/usage or the Master client view GET /client/users/{clientId}/usage.
Timeouts
Only an operation carrying x-client-timeout-seconds publishes a client timeout recommendation through the contract. API 1.71.0 declares 300 seconds for the part-composite, part-damage-transfer and identifier-redaction Vision operations. Do not apply this number globally.
For asynchronous operations, the synchronous request timeout and the job's total processing lifetime are different concepts. Follow the returned job URL and Retry-After until a documented terminal state is reached.
Response handling
- Use request and response data only within the rights and duration agreed for the integration.
- Never share request or response data across Clients or customer-system Tenants.
- Keep raw document and image URLs out of general application logs.
- Record the OpenAPI version, operation ID, input identity and result time with any permitted derived record.
- Do not turn a negative result into a permanent fact.
- Honour operation-specific
Cache-Controlor expiry fields when present.
File, image and document security
- Send only content for which the caller holds the required rights, consents and lawful basis.
- Minimize personal data to the selected purpose.
- Use the exact URL-scheme, size, redirect and media-type constraints of the selected operation; prefer HTTPS wherever that contract permits it.
- Signed query parameters can contain secrets; redact them from logs and provenance views unless the contract explicitly returns a sanitized form.
- Treat OCR and Vision output as derived evidence, not automatically verified master data.
- Keep original media, transformed media, provenance, business approval and publication state as distinct records.
- Never infer that tapinomahub stores or deletes a remote source file on a particular schedule; rely only on an explicitly approved retention policy.
Redirect security
For POST /vin/redirect-sessions:
- create the session from the backend;
- send the API key only in that backend request;
- redirect the browser only to the returned
redirectUrl, before itsexpiresAt(by default ten minutes after creation, configurable between one and sixty minutes); - use a caller-owned, unpredictable
statevalue to correlate the callback when supplied; - verify that returned
stateequals the stored value; - accept only the documented callback parameters
status,tapiIdand, when supplied,state, which extend the query string of the validatedreturnUrlwhile its existing query parameters are preserved; - keep callback state single-use in the customer application.
The contract says an empty state is treated as unset and omitted from the callback. It does not turn state into an authentication credential; the customer application remains responsible for securing its callback session.
Contract pinning and upgrades
Pin generated client types and contract tests to the published info.version. Validate responses against the selected schema: reject or quarantine additive fields when that schema is closed with additionalProperties: false, and accept them only where the schema explicitly permits additions. A transport layer may capture a redacted response for controlled diagnostics when the approved data policy permits it, but the business projection contains only documented fields. Fail validation when a required field, enum or response shape changes unexpectedly.
Do not infer semantic-version guarantees from the version string alone; review contract changes before automating upgrades.