# 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-Key` request header. It applies to every documented operation, including `GET /ping`.
- Intelligence operations additionally declare a verified first-party session cookie as an alternative for explicitly enabled accounts. Server integrations should use `X-Api-Key`; a session-authenticated Intelligence POST must also send `X-CSRF-Token` when that operation declares it.
- 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:

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; 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` | API key missing | Fix secret injection. Do not retry unchanged in a loop. |
| `402` | Billing or sponsorship cannot cover the call | Stop the business operation and resolve funding/terms. |
| `403` | Key invalid, endpoint not enabled, rights missing, or sandbox operation unavailable | Stop and resolve access; 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` | Upload too large or media type unsupported where documented | Reject or transform input before a new request. |
| `422` | Semantically unprocessable or ambiguous input for operations that declare it | Route to review or request better input; do not guess a candidate. |
| `429` | Rate limit or one-active-request rule | 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 base-part response, `part.name` is a string or `null` when available. Preserve the returned value, and do not reconstruct a missing name from another field. `fitment` contains the available type-level assignments and can be empty.

## Error object

The common non-2xx body is:

```json
{
  "error": "stable_machine_code",
  "message": "Human-readable explanation"
}
```

Only `error` is a program key. `message` can be localized or reworded. 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. Adaptive part refresh and VIN Economic Intelligence are currently disabled in production; their initiating routes return the documented error branches when work would have to be started. Apply the polling rows below only after the corresponding feature has been enabled or to a job that was already accepted.

| 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:

```text
send order once
  -> final 2xx: validate and complete
  -> 202:
       validate the required absolute statusUrl against the documented status operation
       compare Location 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 status resource: inspect its domain status
         -> succeeded/final result: require the workflow's usable result, or quarantine
         -> failed: require/map the workflow's embedded error, or quarantine
```

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.

For an operation that declares the header:

1. Generate one opaque key for one logical business action.
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 Intelligence resolver, current-cluster and history GET operations are explicit exceptions to the usual read convention: each requires `Idempotency-Key`. Their two status-polling GET operations accept only `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 initiating Intelligence pilot operations document `X-Tapinoma-Idempotent-Replay` and `409 idempotency_key_conflict`, 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`.
- `false` means the same key returns `409 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 after `Retry-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/registration` and deprecated `/scanner/registration-document`;
- `/scanner/document/registration/international` and deprecated `/scanner/registration-document/v2`;
- `/scanner/document/vehicle` and 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. 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`; repeat an ambiguous request only on the same path contract.

The three key-issuing operations deliberately do **not** support idempotency:

- `POST /client/users`
- `POST /client/users/{clientId}/keys`
- `POST /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`; those two outcomes require an operator-approved recovery procedure.

## Retry policy

Use the global error catalogue, HTTP status and operation contract together.

```text
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 attach to a Client and `endpoint_key`. Client-specific overrides can target one endpoint key or `*`. When no override exists, technical defaults apply, but the numeric defaults are not published.

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.

Recommended queue shape:

```text
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 500
- `offset`: page offset; valid only together with `limit`
- `count`: total result count
- `page.returned`: length of this page
- `page.hasMore`: whether another page follows

API 1.71.0 exposes this pattern on the OE aftermarket-reference list and the VDI catalogue. 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 `items.length === 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 for accepted asynchronous work. |
| `Retry-After` | Minimum seconds before retry or the next poll. |
| `X-Tapinoma-Billing-Source` | Technical settlement decision: `plan`, `balance`, `bundle`, `sandbox` or `idempotent_replay`; this header alone never proves a debit or commercial entitlement. |
| `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 documented time for another live attempt. |
| `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

- Chargeable endpoints have a list price; contractually agreed terms prevail.
- 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 share the documented monthly VIN bundle rule per Client and VIN.
- From 90% plan consumption, the API can emit `X-Tapinoma-Usage-Warning`.
- A pre-charged individual call ending with HTTP status 400 or above is generally refunded exactly once, subject to the two explicit combined-VIN exceptions and the documented no-content rule.

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-Control` or 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`;
- use a caller-owned, unpredictable `state` value to correlate the callback when supplied;
- verify that returned `state` equals the stored value;
- accept only the documented callback fields;
- 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.
