# Redline Autohaus Tuning API integration guide

Contract version **1.2.0** · API namespace **`https://api.redlineautohaus.com/api/v1/tuning`** · [OpenAPI 3.1](./openapi.json) · [Runnable HTTP collection](./examples.http)

RedlineAutohaus.com is the tuning branch of Redline Autohaus & Performance. The Next.js website presents the service; the existing Redline API owns accounts, catalogue rules and file transfer. Redline developer API keys authorize the supported customer Tuning surface and UDS evidence ingestion. Browser sessions remain specific to Tuning; neither credential grants unrelated Firebase or shop access. See the [shared API access guide](../api-access/guide.md). Every example account, file, ID and matching claim below is synthetic. Real production inventory must be supplied and approved by staff.

## Record model and identity

A **file entry** has a title, visibility, access policy and current published-version pointer. A **file version** is one exact immutable uploaded artifact with revision number, source filename, SHA-256, provenance and software associations. A **blob** stores its complete bytes; equal content may share storage, never permissions. Lists/detail queries do not load blob contents.

UUID is canonical identity. A raw software number is deliberately not globally unique: different ECU/calibration/hardware contexts can share the same string. Identifier display preserves zeros, punctuation and case. Phase one normalizes surrounding whitespace only; it does not guess that an unknown scheme is case insensitive. `normalizedValue` is server-owned. Exact `identifier` search never silently broadens to another version. Use `identifierKind` and `identifierNamespace` to disambiguate.

Vehicle fitment uses individually verified release-to-variant evidence. A filename, ECU family or vehicle match is not proof of flash compatibility. A2L definitions may have multiple separately verified release links. Candidate links remain visibly candidate. The service stores files; it does not generate Stage 1, repair checksums or flash ECUs.

## Roles and endpoint permissions

| Permission        | Account requirements                                                       | Endpoint families                                                                                                               |
| ----------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| Visitor           | No session                                                                 | Public `/site`, `/catalog/*`, `/files*` metadata, `/software-releases*`; trusted-origin public authentication POSTs             |
| Customer          | Non-suspended tuning session; unverified accounts may manage their account | `/me`, `/me/sessions`, password/session controls, own history/grants                                                            |
| Customer download | Active and email verified                                                  | Current public published bytes with member policy or active manual grant                                                        |
| Editor            | Active, verified editor or admin                                           | `/admin/catalog/*`, `/admin/software-releases*`, staff file inspection/upload/unlocked edits/review and exact-version downloads |
| Administrator     | Active, verified admin                                                     | Publication/archive, user roles/status, grants, audit/site changes; public-facing edits of already published entries            |

The complete per-operation matrix is in each OpenAPI operation's `x-role`, security, headers and description. `/admin/users` and grants are admin-only. Editors cannot publish, grant access or change public metadata of an already published entry. Staff cannot bypass their own suspended/unverified status. Frontend route guards do not provide authorization.

## Company profile and EVC / WinOLS licence reference

An account may add or remove an optional public EVC licence reference through the ETag-protected profile update. `evcLicense` accepts the official `https://www.evc.de/en/check_evc_license.asp?k=…` link or the official linked badge HTML. The API rejects other hosts, endpoints, scripts, event handlers, extra markup, duplicate query parameters and mismatched link/image keys. It stores only a normalized public key and returns generated `checkUrl` and `imageUrl` values; it never stores or serves user HTML.

For a currently published file with proven uploader provenance, `version.companyAttribution` can include that official link/image pair for Original, Tuned, A2L or document records. Attribution remains tied to the visible exact version. An accepted same-uploader replacement can retain attribution when its kind and exact blob hash/size match the immutable canonical origin and its accepted canonical exact-hash fingerprint. Shared bytes from another uploader, unfinished/duplicate items, private or withdrawn versions and unresolved fingerprints do not qualify.

The EVC reference is independent of Redline's `companyVerified` decision. Adding it grants no role, access, verification, reward, publication or download privilege. Neither badge certifies a file, tune, ECU match, flash result or safety.

## Browser session, cookies, CORS and CSRF

```javascript
const base = "https://api.redlineautohaus.com/api/v1/tuning";
const loginResponse = await fetch(`${base}/auth/login`, {
  method: "POST",
  credentials: "include",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    email: userEnteredEmail,
    password: userEnteredPassword,
  }),
});
if (!loginResponse.ok) throw await loginResponse.json();
const {
  data: { user, csrfToken },
} = await loginResponse.json();
```

The API sets `__Host-redline_tuning_session` with Secure, HttpOnly, Path=/, SameSite=Lax and **no Domain**. Both production hosts use HTTPS. They are different origins even though same-site. Never put cookies/session tokens in localStorage or widen Domain for Next.js SSR. Next.js cannot read the API-host cookie; personalized screens obtain browser session state directly from the API.

On reload call `GET /auth/session` with credentials. It returns sanitized user, permissions, stable `csrfToken` and absolute `expiresAt`, or 401. Ordinary reads do not rotate CSRF, so concurrent tabs can submit safely. Login and privilege transitions rotate/revoke session state. Customer sessions default to 24-hour idle / 7-day absolute expiry; staff 1-hour idle / 12-hour absolute expiry. Server expiry is authoritative.

Authenticated POST/PATCH/PUT/DELETE require `X-CSRF-Token`, the cookie and an exact approved Origin. Pre-login POSTs require trusted Origin, JSON and shared throttling. Browsers set Origin automatically; CLI clients send the approved origin explicitly. SameSite alone does not stop sibling-subdomain CSRF. Bodyless logout/logout-all/session DELETE do not need JSON Content-Type.

CORS permits only configured exact frontend origins, with credentials. Allowed headers: `Content-Type`, `X-CSRF-Token`, `If-Match`, `Idempotency-Key`. Exposed headers: `ETag`, `X-Request-Id`, `Retry-After`, `Content-Disposition`. Do not use `*` with credentials. A typical preflight is:

```http
OPTIONS /api/v1/tuning/me
Origin: https://redlineautohaus.com
Access-Control-Request-Method: PATCH
Access-Control-Request-Headers: content-type,x-csrf-token,if-match
```

Registration is always customer-only; role/hash fields are rejected. Duplicate registration and verification/recovery requests return the same generic202. Verification links expire after24hours; reset links after30minutes. Tokens are single-use and hashed at rest; a replacement invalidates the previous token. Email links place the secret in a URL fragment (`/verify-email#token=...` or `/reset-password#token=...`) so browsers do not send it to HTTP/access logs. The frontend reads and clears the fragment from the address bar. A GET never consumes the token: the frontend sends an explicit trusted-origin POST. Reset and password changes revoke existing sessions and require fresh login. Logout clears the API cookie; logout-all revokes all devices. A user can revoke only their own session IDs. `/me/sessions` is paginated with the common page/pageSize parameters (default25, maximum100), stable creation-time/UUID ordering, and a visibility-scoped total; expired/revoked and other-user sessions do not count.

Passwords support spaces/password managers and 15–128 Unicode characters, without trimming or composition rules. Hashes are Argon2id, baseline19MiB/2iterations/parallelism1, never exposed. Login limits default to10/account and30/IP per15minutes; register/recovery/resend5/email and20/IP perhour, using shared counters across replicas. 429 includes Retry-After; account limits expire rather than create indefinite stranger-triggered locks.

## Catalogue and access decisions

`GET /files` supports `q`, `identifier`, `identifierKind`, `identifierNamespace`, `softwareReleaseId`, `ecuManufacturerId`, `ecuTypeId`, `makeId`, `modelId`, `vehicleVariantId`, `year`, `kind`, `stage`, `accessPolicy`, `sort`, `page`, `pageSize`. Filters are ANDed. Vehicle filters use verified fitments; unknown year ranges do not claim a match. Changing a make should clear incompatible model/variant selections in the UI.

Sorts: `relevance` (default with search), `updated` (default otherwise), `title`, `size`, `published`. Pagination starts at1, defaults to25 and caps at100, with stable UUID tie-breaking. Lists return `{data:[...],page:{page,pageSize,totalItems,totalPages,hasNextPage}}`. An unknown identifier returns a true empty list, never a fallback guessed release. `/files/facets` returns distinct file counts under the same visibility/filter scope, including visible locked listings without revealing private records.

```json
{
  "data": [],
  "page": {
    "page": 1,
    "pageSize": 25,
    "totalItems": 0,
    "totalPages": 0,
    "hasNextPage": false
  }
}
```

Access decisions use these exact reason values:

| Reason                        | UI action                              |
| ----------------------------- | -------------------------------------- |
| `SIGN_IN_REQUIRED`            | Sign in to download                    |
| `EMAIL_VERIFICATION_REQUIRED` | Verify email to download               |
| `ALLOWED`                     | Download the recorded BIN/A2L/document |
| `ACCESS_REQUIRED`             | Contact Redline for access             |
| `PURCHASE_NOT_AVAILABLE`      | Purchasing coming soon                 |
| `FILE_UNAVAILABLE`            | Unavailable; no download               |

`canDownload` is advisory; the byte endpoint independently rechecks. Customer access requires active verified account, public/published entry, valid current bytes, and `members` or an active manual grant. A `staff_only` policy always overrides a customer grant. `paid_coming_soon` allows a manually granted customer only. Archive overrides grants. No checkout, charge, order, credit or payment-success route exists; unknown purchase routes return404. Planned price is integer CAD minor units, never entitlement.

Related records each enforce visibility. Public detail omits raw hidden baseline/prior identifiers; `/files/{fileId}/related` returns only visible exact associations. `versionId`, `versionLabel`, `isCurrentPublished` retain historical identity for `baseline`, `previous_revision`, `definition_for` and `documentation_for`. They never substitute a newer version. Historic customer bytes are unavailable; staff use exact-version download.

All personalized responses and binaries use `Cache-Control: private, no-store`. Do not let a shared CDN cache one user's access result.

## Guided staff workflow

1. Create/select manufacturer, ECU type and a UUID software release with reliable identifiers. A verified release needs recorded evidence; manual identifiers work without guessing bytes.
2. Upload exactly one artifact with metadata. New entries are staff-only drafts. Inspect server SHA-256/byteSize and handle duplicate warnings.
3. Record read method/tool and full/partial/virtual/unknown read type. Associate verified/candidate release and vehicle evidence deliberately.
4. For a tune, select the exact ready original version with the same verified release, then stage and fuel/hardware requirements or explicit “not specified”. A baseline becomes immutable in the same transaction. A different software release belongs in a separate file entry.
5. For definitions/documents, record exact target versions and evidence. An A2L definition requires a BIN target and shared verified release; verified targets are ready and locked. No self/cyclic or false-kind associations.
6. Review unlocked revision with `{reviewState:'ready',validationNotes:'...'}`. Ready checks integrity and metadata, not ECU testing. Editors may prepare this state.
7. An administrator previews, deliberately chooses access/visibility and publishes the own ready version using the current file ETag plus idempotency key. The version pointer and audit update atomically.

Uploading revision2 preserves revision1's bytes and current pointer until explicit publication. Version numbers allocate under a file row lock. Bytes/blob/file ownership and revision number are immediately immutable; publication/baseline/verified-association use locks identity/provenance. Corrections require a new version/release. Release/ECU identity cannot silently relabel historically relied-on bytes. Re-publishing an older ready version is an explicit rollback through the publish endpoint. Archive hides and blocks customer access while preserving history.

The staff detail's `version` is current published, otherwise latest draft. Use the paginated `/versions` list to inspect all revisions. Upload responses include newly created `version`, `fileId`, `versionId`, `sha256`, decimal-string `byteSize`, `duplicateContent`, and file ETag; their `file` retains current publication state.

## Multipart upload and retry

Send uploads **directly to the API**, never through Next.js Server Actions/functions. A multipart request has one `metadata` UTF-8 JSON field and one `file` part. Extensions: `.bin`, `.a2l`, `.pdf`, `.txt`, `.md`; no archives, executable/HTML or public customer uploads. BIN has no universal magic signature, so accepted upload does not mean ECU compatibility verification.

Defaults:64MiB BIN/A2L,10MiB document,65MiB total request,64KiB metadata. `/site.uploadLimits` publishes actual configured caps. At most two upload/content writes and two downloads per instance initially. 429+Retry-After means admission capacity is occupied; 413 means size limit. Bounded private temp files are cleaned on success/failure/disconnect. Multipart parts/metadata lengths and actual bytes are checked even without Content-Length. A2L encoding, BOMs, non-ASCII and CR/LF bytes remain unchanged.

The HTTP collection contains original, tuned, A2L and PDF examples. A curl form example:

```sh
curl --fail-with-body "$BASE/admin/files" \
  --cookie "$COOKIE_JAR" \
  -H "Origin: $ORIGIN" -H "X-CSRF-Token: $CSRF" \
  -H "Idempotency-Key: $OPERATION_KEY" \
  --form 'metadata={"kind":"original_bin","title":"Synthetic original read","versionLabel":"Read 1","readType":"full","readMethod":"bench"};type=application/json' \
  --form 'file=@./synthetic-original.bin;type=application/octet-stream'
```

This creates a draft; a verified release association and review are still needed before publication. Tune/A2L verified examples reference real staff-selected UUIDs in your environment, not the illustrative IDs in this guide.

Retry an interrupted/uncertain upload with the **same key and identical metadata, filename and bytes**. It returns one business outcome, not another version. Same key plus changed payload returns409 `IDEMPOTENCY_CONFLICT`. Different key plus identical bytes requires staff acknowledgement:409 `DUPLICATE_CONTENT` exposes only staff-visible matching entries. Confirm by adding `acknowledgeDuplicate:true` and generating a **new** key because metadata changed. Duplicate storage never grants access to another entry. There is no public global hash lookup.

## ETags and lost-update protection

Mutable detail GETs return `ETag: "1"`; list records include their own `etag`. Send the resource's current value in If-Match on PATCH, link replacement, review, publish, archive and grant changes. A collection ETag does not authorize editing a member. Missing If-Match returns428, stale returns412; fetch current metadata and reconcile instead of blindly overwriting.

Use **file ETag** for file patch/revision upload/publish/archive; use **version ETag** for version metadata/release links/relations/review; use **release ETag** for identifiers/fitments. Grant detail has its own GET/ETag. Successful mutations increment rowVersion and return the new ETag. Upload/version creation, publish and grant creation also require an actor/operation-scoped Idempotency-Key retained24hours. Login/session secrets never enter idempotency records.

## Manual grants and administrator safety

POST `/admin/users/{userId}/grants` accepts `{fileId,reason,expiresAt?}` with a unique idempotency key. One current row per user/file; an existing row returns409 `GRANT_EXISTS` with permitted `fieldErrors.grantId`. It does not silently upsert. GET `/admin/grants/{grantId}` obtains its ETag. PATCH accepts `action:update|revoke|reactivate`, reason and optional future expiry. Reactivate clears revokedAt, sets new expiry or no expiry, records the current grantor and audits a new decision. Update alone does not reactivate.

Role/status changes are explicit admin operations, revoke sessions and cannot activate an unverified email. Concurrent changes cannot remove the last active verified admin. There is no account/file/version/blob hard-delete API. Initial-admin bootstrap, migration credentials, cleanup and operational recovery are documented in the private source handover, not served here.

## Raw downloads and integrity

`GET /files/{fileId}/download` sends the current allowed bytes. Staff exact-version GET is `/admin/files/{fileId}/versions/{versionId}/download`. Only GET materializes content; HEAD returns405 with `Allow: GET`. Range/resume is deferred: responses are complete200 and do not advertise Accept-Ranges.

BIN uses application/octet-stream; all artifacts are attachments with sanitized `filename` plus UTF-8 `filename*`, exact Content-Length, private/no-store and nosniff. Never embed a document as executable HTML. A top-level attachment navigation carries the API cookie without JavaScript buffering. A fetch client must check non-2xx problem JSON before creating a Blob; never decode file bytes as text/JSON.

```sh
# Use your authenticated cookie jar. --fail prevents JSON errors becoming a .bin.
curl --fail-with-body --cookie "$COOKIE_JAR" \
  --output ./downloaded.bin "$BASE/files/$FILE_ID/download"
sha256sum ./downloaded.bin
# Compare against version.sha256 from the same current file detail.
# To prove a synthetic roundtrip:
sha256sum ./synthetic-original.bin ./downloaded.bin
cmp ./synthetic-original.bin ./downloaded.bin
```

Do not compare against a newer file detail if another administrator published between lookup and download; repeat with the current version or use an authorized exact staff version. Download history records started/completed/aborted/failed and bytes sent. completed confirms API response completion, not that the browser saved the file.

## Problems, recovery and observability

All errors use `application/problem+json`: `type`, `title`, `status`, `detail`, `instance`, `code`, `requestId`, optional `fieldErrors`. Request IDs are also returned as `X-Request-Id`. Keep that ID when reporting issues; do not log passwords, cookies, tokens, reset URLs or file contents.

| HTTP | Typical meaning                                                                                   |
| ---- | ------------------------------------------------------------------------------------------------- |
| 400  | Malformed JSON/metadata/filter/UUID or invalid/expired token                                      |
| 401  | Missing/expired session or generic invalid credentials                                            |
| 403  | Role/account/CSRF/Origin/file policy denied                                                       |
| 404  | Unknown or nonvisible resource; no existence leak                                                 |
| 405  | HEAD not permitted on byte downloads; use GET                                                     |
| 409  | Idempotency conflict, duplicate content/grant, immutable provenance or last-admin protection      |
| 412  | Stale If-Match; refresh/reconcile                                                                 |
| 413  | Configured request/file/metadata size exceeded                                                    |
| 415  | Wrong JSON/multipart media or unsupported extension/content                                       |
| 422  | Invalid baseline, software cardinality, association evidence or review/publication state          |
| 428  | Missing required If-Match                                                                         |
| 429  | Authentication/admission throttling; obey Retry-After                                             |
| 503  | Tuning disabled, database unavailable or temporarily saturated; other API modules remain isolated |

Inspect machine `code`, not English text. Codes include `INVALID_CREDENTIALS`, `TOKEN_INVALID`, `ORIGIN_NOT_ALLOWED`, `CSRF_INVALID`, `IF_MATCH_REQUIRED`, `PRECONDITION_FAILED`, `IDEMPOTENCY_CONFLICT`, `DUPLICATE_CONTENT`, `GRANT_EXISTS`, `VERSION_IMMUTABLE`, `RELEASE_IMMUTABLE`, `ECU_IDENTITY_IMMUTABLE`, `LAST_ADMIN` and the six access reasons above. Recoverable transport/admission errors may be retried with the original idempotency key; semantic errors require correcting the request.

## Versioning and public reference safety

The checked-in1.0.0 contract specifies every concrete route, parameter, request schema, response, security requirement and example. Public interactive docs execute anonymous metadata GETs only, without cookies. They cannot upload, change accounts or initiate binary transfers. Use your own configured client for authenticated examples.

Compatible v1 changes may add optional fields, endpoints and documented nonbreaking metadata. Renaming/removing fields, altering required request semantics or weakening/strengthening authorization unexpectedly requires a new major namespace/contract with a migration notice and agreed overlap period. There is no fixed deprecation date for this first release; communicate one before retiring a version. Unknown added fields should not crash tolerant clients. The server continues to reject unknown mutation input fields to prevent mass assignment.

## Contribution and credit workflows

The add-on preserves the existing session/CSRF/origin and visibility rules. Read `GET /contribution-config` before presenting submissions: controls may be disabled, production rates may be unconfigured, and rights text must have an approved current version. All examples are synthetic. The service worker contract is separate and must never be used with a customer cookie.

1. `POST /contributions` creates an owned draft. Upload each item with multipart `metadata={clientItemId,kind,claims}` plus one unchanged file. Stock needs no dependency; A2L needs stock; tune needs stock and matching A2L. Dependency search is metadata-only and grants no bytes.
2. Save claims/dependency selections using the current item ETag. `POST /contributions/{id}/submit` uses the contribution ETag, snapshots consent and returns 202 with persisted item/job IDs. Uploading is not submission or acceptance.
3. Read `GET /contributions/{id}` after refresh. A credentialed `GET /contributions/{id}/events` emits `event: verification` with a monotonic contribution cursor. Reconnect with `Last-Event-ID` or `after`; polling recovers the authoritative same state after disconnect or event retention. Never infer a percentage from a stage name.
4. Keep verification (`queued`, `processing`, `waiting_dependencies`, `needs_review`, `failed`, `accepted`, `duplicate`, `rejected`, `withdrawn`), publication and reward states separate. Pending rewards cannot be spent. Every tune requires staff review. Customer reports do not reveal protected dependency contents; staff sees the attributable evidence.
5. `POST /files/{fileId}/download-quote` with `versionId` snapshots one exact price without reserving funds. Display version, price, balance, remaining balance and expiry; confirm before `POST /download-acquisitions` with its `quoteId`. No dependency is purchased automatically.
6. Retry a lost acquisition response with its original body and Idempotency-Key. Permanent user/version uniqueness also prevents another debit with a new key. `GET /files/{fileId}/versions/{versionId}/download` never debits and never substitutes a newer version. Repeat a failed transfer under existing access.

For a retried mutation, preserve the original Idempotency-Key, body and If-Match value. Changed intent requires a new key and refreshed ETag. A stale-state 412 requires reloading and reviewing current data; do not silently overwrite another change. New acquisitions can be disabled while permitted existing redownloads continue. Temporary manual grants and staff access are free but do not create a permanent entitlement; refresh a quote if that access changes.

### Add-on reason codes

An HTTP failure uses the existing problem response. Verification/decision codes may instead accompany a successfully persisted outcome. Customer reports can use fixed safe finding labels rather than private staff explanations.

| Code | Meaning / client response |
| --- | --- |
| `MISSING_STOCK_DEPENDENCY` | Select or upload the required exact stock baseline. |
| `MISSING_A2L_DEPENDENCY` | A tune also requires its exact matching A2L. |
| `DEPENDENCY_NOT_ACCEPTED` | Wait for acceptance or select an available accepted dependency. |
| `A2L_STOCK_MISMATCH` | Select an A2L verified against the tune's same stock baseline. |
| `UNSUPPORTED_PROFILE` | The check is unqualified for this family/layout; retain staff review. |
| `INCOMPLETE_A2L` | Required parsing/address evidence is incomplete; no verified match is implied. |
| `DUPLICATE_CONTRIBUTION` | Completed duplicate decision; no second reward. |
| `VERIFICATION_RETRYABLE` | Retry the failed attempt before requesting acceptance. |
| `QUOTE_EXPIRED` | Refresh the quote and obtain a new confirmation. |
| `QUOTE_ACCESS_CHANGED` | Refresh a formerly free quote after grant/role access changed. |
| `INSUFFICIENT_CREDITS` | No debit; offer the contribution workflow. |
| `CREDIT_ENTITLEMENT_REQUIRED` | Exact-version acquisition or a current manual grant is required. |
| `VERSION_WITHDRAWN` | Historical access does not permit this withdrawn version's bytes. |
| `DEPENDENCY_REVIEW_HOLD` | A dependency needs staff review; new dependent access is blocked. |

Preserve existing 401/403/404 privacy behavior and 409/412/413/415/422/428/429 handling. Display request IDs for support; do not infer private duplicate identities from errors. Staff withdrawal returns affected versions and account review lists. Refunds and reward reversals are explicit audited actions, each allowed once; they do not delete history or restore consumed canonical reward eligibility.
