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

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:

  1. records the operation ID, HTTP method and path template without recording secrets;
  2. captures HTTP status and the documented operational headers;
  3. parses JSON only when the response media type and operation declare it;
  4. on non-2xx, branches on the stable top-level error code and never on message;
  5. on a job-status 200, branches again on the job's domain status;
  6. rejects a payload that does not satisfy the pinned OpenAPI schema;
  7. 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:

  1. 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 returns 400 invalid_idempotency_key.
  2. Persist it before sending the request.
  3. Reuse the same key after a timeout or other ambiguous transport failure.
  4. Never reuse it for a different request.
  5. 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:

The renamed scanner paths use compatibility aliases rather than HTTP redirects. Follow the documented request, response and idempotency behaviour for each alias pair:

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:

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:

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

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

File, image and document security

Redirect security

For POST /vin/redirect-sessions:

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.