# CarSoft partner integration guide

Contract: `partner-openapi.json`, version `0.10.0-draft`. This guide describes the local implementation under verification. It is not a released API or an invitation to use production dealership data. The intended API origin is `https://app.carsoft.ai`; production partner access remains disabled.

## Apply and obtain approval

Describe your organization, beneficial ownership, app, hosting, support/security contacts, data use/storage/subprocessors, retention/deletion, AI use, incident process, commercial terms, execution model and requested permissions. Declare any financial capabilities separately. Your application is a request for manual review; receipt or screening does not provide credentials or production approval.

CarSoft manually reviews each application and material revision. The approved manifest records exact public fields, scope ceiling, redirects, embedded origin and locations, webhook origins, retention exceptions and pricing. The local platform-admin workflow verifies each exact redirect, webhook and embedded hostname before app approval or credential issuance. After screening, an administrator requests a one-hour DNS TXT challenge at `_carsoft-partner.<hostname>` and provides its random `carsoft-verification=...` value to your domain operator. Publish that exact value; verification uses the server's DNS resolver with a five-second deadline. Multiple TXT chunks within one record are joined. A rotated, expired or consumed challenge cannot be reused.

Successful proof lasts 30 days. A parent domain does not authorize subdomains, and another organization cannot inherit your proof. Renew before expiry; requesting renewal preserves an existing valid proof. Expiry or withdrawal blocks installation, authorization, API access, embedded rendering and event delivery. Reverification after expiry/withdrawal invalidates old OAuth families; obtain fresh dealership authorization. Submitted payments can still reconcile internally. DNS ownership does not replace manual security review or outbound public-address checks. Complete self-service developer accounts, enrollment/MFA/team recovery, isolated sandboxes and renewal alerts remain unfinished.

Dealership administrators separately install an approved revision for one dealership, grant a subset of permissions, accept disclosed terms and configure financial limits. Partner approval alone never provides access to all dealerships. CarSoft reserves destructive actions, staff/role management, platform administration, platform subscription/security changes and credential access to the platform.

## Authorization

Use a confidential backend client. Secrets and tokens belong on your server. Browser scripts, iframes, public repositories, URLs and logs must not contain them. Public clients and client-credentials/password grants are unavailable in this draft.

1. Obtain a client ID, exact registered redirect and secret through approved credential issuance.
2. Generate a cryptographically random state and PKCE verifier. Store them as a short-lived, one-use transaction bound to the person initiating the connection. Derive the S256 challenge as base64url(SHA-256(verifier)).
3. Direct the dealership user to `/partners/authorize` with `response_type=code`, `client_id`, exact `redirect_uri`, space-separated `scope`, `state`, `code_challenge` and `code_challenge_method=S256`.
4. The user reviews access and reauthenticates. Interactive execution follows their current permissions; only a dealership administrator can authorize background execution under the installation's grant.
5. On your registered callback, compare state to your stored transaction before exchanging the code. Never accept an unsolicited callback or reuse state.
6. POST form-encoded `grant_type=authorization_code`, `client_id`, `client_secret`, `code`, `redirect_uri` and `code_verifier` to `/api/partners/oauth/token`.
7. Store returned tokens in protected server storage. Send `Authorization: Bearer <access_token>` to the resource API.

Authorization codes last at most 120 seconds and redeem once. Access tokens last at most 15 minutes; use the returned `expires_in`. Refresh tokens last at most 30 days and rotate on every successful refresh. POST `grant_type=refresh_token`, `client_id`, `client_secret` and the current `refresh_token`; optionally request a reduced scope set. Atomically replace stored tokens and serialize refresh per connection. Reuse of an already consumed refresh token revokes its entire family. A lost refresh response requires a new authorization flow rather than repeated replay.

Credential rotation immediately invalidates the old secret, pending codes and existing client token families. Revocation is permanent. Dealer consent/configuration changes also revoke existing tokens and pending codes, disable subscriptions and cancel unsubmitted financial requests. Request fresh authorization and recreate subscriptions. Changes to an installed app revision require dealership-admin consent; new permissions start unselected in the review UI. Neither a review nor a pricing change silently expands access.

On `401`, resolve expiry/revocation through a valid refresh or fresh authorization. On `403`, obtain the missing approval/consent rather than retrying with guessed identifiers. Partner credentials do not replace CarSoft browser sessions.

## Resource requests

The implemented resource paths are `/api/partners/v1/inventory`, `/leads`, `/clients`, `/service`, `/parts` and `/appointments`, with individual record routes at `/{id}`. Each operation requires its `resource:read`, `resource:create` or `resource:update` permission. The OpenAPI artifact is authoritative for accepted fields and required values. Read projections include only approved public fields plus ID/version; write permissions do not authorize hidden fields.

GET lists accept only `limit` (1–100, default 25) and `after`. Follow `pagination.next`; do not assume a complete export or stable snapshot. Reads return versions as `partner_revision` and ETags for individual records.

POST creates require an `Idempotency-Key` of 8–128 characters from letters, digits, `.`, `_`, `:`, `-`. Generate it once per intended effect and store it before sending. Retry the exact intent with the same key after a lost response. Reusing a key with different content returns `409`. PATCH also requires `If-Match: "<current version>"`. A stale version returns `409`; reread and resolve the change rather than forcing an overwrite. No resource DELETE is provided.

CarSoft allocates inventory stock codes, vehicle titles and parts SKU codes. Do not invent these fields. Dates, text lengths, enumerations, monetary representations and relationship identifiers are validated; fields outside the approved policy are rejected. Tenant identity never comes from your payload.

Current local throttles are 600 requests/app/minute and 120 requests/installation/minute, with a separate ingress limit. `429` carries `Retry-After: 60`. These are implementation defaults under verification, not a public availability commitment. Use bounded backoff, concurrency limits and per-installation queues.

## Customer invoices

`invoices:read` permits customer-facing invoice projections. `invoices:create` creates customer-only drafts for existing authorized customers. Send `clientId`, `department` (`Vehicles`, `Service`, `Parts`) and 1–100 lines. Lines use `kind` (`Labor`, `Part`, `Fee`, `Discount`, `Tax`), `name`, positive `qty` (at most three decimal places) and nonnegative integer `unitAmountMinor`. USD is the current implemented currency. Aggregate totals are rounded once to minor units; discount lines reduce the total.

CarSoft derives the customer name and allocates an `APP-` number in a separate namespace. Apps cannot supply receipts, notes, arbitrary payer classifications, internal/warranty lines or merchant secrets. `invoices:draft-update` replaces the customer/department/lines of a draft created by that same installation, using the current ETag and an idempotency key. Sent, paid, voided or payment-linked drafts are locked. Financial adjustments and dispute interfaces are not available merely because invoice reads are available.

## Financial requests

Ordinary apps request financial actions through CarSoft; only independently reviewed processor adapters connect to merchant providers. Funds go directly through the dealership's merchant provider. Partners collect their own app fees and CarSoft separately bills partners under approved commercial agreements. Installing an app does not create a CarSoft-collected partner subscription.

Each financial capability is separately granted. POST `/api/partners/v1/payments/links`, `/charges` or `/refunds` requires the matching scope, an idempotency key, `invoiceId`, reviewed `processorId`, positive integer `amountMinor` and `currency: "usd"`. Charges additionally require an authorized `savedMethodId`; refunds require the original confirmed `parentId`.

A saved method is a server-protected reference backed by documented customer authorization and amount/currency restrictions. Apps never receive raw card/bank details or merchant keys. An API request cannot supply consent on behalf of the customer. Actual customer setup/onboarding and provider acceptance remain incomplete in this draft.

A payment intent is stable across retries. The response reports its state, not a claim that funds have settled. Payment links must stay on the processor’s explicitly reviewed public HTTPS origins. `reserved` means not submitted; `submitting`, `pending`, `requires_action` or `unknown` may involve a provider effect. Use the same intent/key and status lookup; never create a replacement charge to resolve a timeout. `succeeded` requires a verified merchant/intent/invoice/amount/currency/provider identity and a durable outcome. `failed` is terminal only when supported by verified provider state. `approval_required` returns HTTP 202 for one exact refund awaiting dealership-admin decision.

Automatic refunds obey per-refund and cumulative daily limits and the original payment's remaining refundable amount. Splitting refunds does not bypass the cumulative limit. Above-limit approvals name an exact immutable refund, expire after 15 minutes and are rechecked at submission. Submission-day quotas also apply to requests reserved on an earlier day. Saved-method charges recheck current customer consent, merchant connection, processor status, app access and current actor permissions before submission.

GET `/api/partners/v1/payments/{id}` with `payments:read` returns only this installation's financial intent. POST `/{id}/cancel` with `{ "operation": "link" | "charge" | "refund" }` and the corresponding action permission can cancel an unsubmitted reserved/approval-required request. Repeating cancellation is safe. Already submitted effects cannot be canceled with this endpoint; reconciliation/refund flows apply. It does not delete invoices or records.

Processor adapters and callbacks have not completed sandbox acceptance. Additional currencies, submitted-payment cancellation/expiry, authentication-required recovery, adjustments, disputes and full saved-method onboarding remain incomplete. Do not infer support from the generic payment state names.

## Events

POST `/api/partners/events/subscriptions` with `events:subscribe`, each requested resource's read permission, an exact reviewed HTTPS endpoint and a unique resource list. Resources include the six operational resources plus `invoices` and `payments`. The one-time `signingSecret` belongs in protected server storage. Subscriptions deliver only future events. Financial payment events belong to their originating installation; no merchant secrets or payment tokens are included.

Delivery is at least once; ordering is not guaranteed. An envelope has string `id`, `version: "1"`, `type: "<resource>.<created|updated>"`, `occurredAt` and `data: { resource, recordId, revision }`. Treat the event as a reference and fetch the record through the authorized API. Use the record revision to avoid overwriting newer local state.

Validate `CarSoft-Signature: t=<Unix seconds>,n=<nonce>,v1=<hex HMAC>` against the exact raw UTF-8 body. Compute HMAC-SHA256(signingSecret, `timestamp + "." + nonce + "." + rawBody`) and compare in constant time. Reject timestamps more than five minutes from your synchronized clock. Persist/deduplicate the event ID atomically before applying effects; do not rely on timestamp validation alone. Enqueue durably before returning 2xx. Avoid holding delivery open for long processing.

GET the subscription collection for bounded current subscriptions; GET `/{id}` for up to 100 delivery summaries. PATCH `/{id}` with `{ "action": "disable" }` or `{ "action": "rotate-secret" }`. Rotation immediately replaces the signing secret and additionally requires current read access to the subscribed resources. Use a controlled transition and durable deduplication. Automated replay and full operational recovery controls remain incomplete.

CarSoft checks current permissions before delivery and retries; eight attempts lead to a dead state. HTTPS destinations resolve exclusively to public addresses, are pinned for each request, and redirects are not followed. DNS, connection/absolute durations and response size are bounded. Revocation cannot retract previously received events.

## Isolated embedded UI

The currently implemented location is the dedicated Apps area. Other declared locations are still pending. Your reviewed UI origin must be separate from the entire `carsoft.ai` cookie site. Partner scripts do not run in the host page. The iframe permits scripts, forms and its own origin while restricting host navigation, popups and sensitive browser features.

The current bridge accepts only `{ "type": "carsoft.context", "requestId": "<1–64 letters/digits/underscore/hyphen>" }`. The host checks exact sender origin and frame window identity. The response contains app/installation IDs and locale, never host session credentials or broad API tokens. IDs are context, not authorization. Your backend still needs its approved OAuth connection. Do not assume navigation/download/clipboard/financial host actions or a complete extension SDK.

## Disconnection, data use and support

Disconnect/suspension prevents new partner actions. Unsubmitted financial work is canceled; submitted external effects still reconcile internally without restoring app access. Retain required transaction identities for that reconciliation under the reviewed policy. A processor switch must not abandon old transactions.

Use data only for the approved purpose and permitted retention. Delete partner-held data, including subprocessors/copies, within 30 days after disconnection, subject only to specifically reviewed retention exceptions. Provide a deletion attestation and evidence when requested. API revocation cannot prove deletion in your systems.

Partner support handles app defects; CarSoft handles platform failures, access enforcement and suspensions. Include the `X-Request-ID` and structured error code in a support report without tokens, secrets, raw payment data or unrelated customer records. Rotate/revoke compromised credentials immediately and follow the approved incident process.

## Release and verification boundary

This guide accompanies a draft artifact and synthetic local verification. It does not establish a complete developer program, production deployment, sandbox credentials, provider acceptance, independent security assessment, operational service promise or native-app compatibility. CarSoft's launch acceptance matrix must be completed before production partner access is offered.

## Bounded export and bulk jobs (0.4.0 draft)

These local routes are synchronous bounded jobs, with a persisted result and no delayed write queue. Large queued transfers and attachments remain unfinished parts of the full marketplace goal.

An export needs separately approved and dealer-consented `exports:create` plus the source resource's `:read` scope. POST `/api/partners/v1/exports`, using `Idempotency-Key`, with `{ "resource": "clients", "limit": 100, "after": "" }`. The limit is 1–500 records (default 100); only the six operational resources are available. Output uses the approved resource field policy and current actor permissions. Arbitrary fields, dealership IDs, financial records, staff, filters and private documents are rejected. Five new export captures per installation per minute are allowed; the ordinary API rates also apply. Output above 2 MiB is rejected; reduce the requested count.

The response contains a job UUID and 15-minute expiry. GET `/api/partners/v1/jobs/{id}` reads its metadata. GET `/api/partners/v1/jobs/{id}/download` returns an authenticated, no-store JSONL attachment, one record per line, with no public storage URL. When more records exist, `X-CarSoft-Next-Cursor` identifies where the next capture should start. These bounded captures are not a guaranteed point-in-time snapshot of an entire large data set across separate requests.

The creating installation, consent version, actor and OAuth refresh family are bound to the artifact. A refresh within that family can retrieve it; a new independent authorization cannot inherit it. Export and resource permissions are checked again at download. Suspension, credential/family revocation, offboarding, disconnection and expiry prevent subsequent access. Re-consent/disconnection clears stored results immediately. PATCH the metadata route with `{ "action": "cancel" }` to clear the result. Download cannot recall data a partner has already received; the approved use and deletion obligations still apply.

A bulk job needs explicit `resource:bulk` approval and consent, plus each requested `:create` or `:update` scope and current actor write permission. POST `/api/partners/v1/bulk`, using `Idempotency-Key`, with `{ "resource": "clients", "items": [ { "operation": "create", "input": { "name": "Example customer" } }, { "operation": "update", "id": "existing-id", "version": 2, "input": { "name": "Updated name" } } ] }`. The batch contains 1–25 items and the request is limited to 64 KiB. Update versions are integers from the record's current `partner_revision`; stale versions fail. Deletion, payment/invoice operations, chosen create IDs and private fields are unavailable.

Whole-body schema validation and action-permission preflight happen before any writes. During execution each item uses the existing tenant/approved-field/version checks and its own atomic subtransaction. Successful items persist; individual record conflicts/forbidden approved fields return safe per-item error codes without SQL details. Results preserve input indices and operations. Repeat the same key and body to retrieve the original result without re-executing successful writes. A changed body conflicts. Result expiry or cancellation never undoes completed changes. Ten new bulk jobs per installation per minute are allowed; every item also consumes the app's and installation's ordinary request allowances. These limits do not grant additional scopes or access.

Operators must schedule the separately authenticated POST `/api/partners/internal/jobs` cleanup to clear expired result payloads (at most 1,000 per call, using non-blocking claims; repeat while work remains); expiry blocks reads immediately even before cleanup. Hosting, cleanup cadence/alerts, large queued work, sustained load and recovery acceptance are still launch requirements. Job metadata/idempotency records remain for audit and retry denial; canceled/expired keys do not create a fresh operation. Select a new key only for a genuinely new intent.


## Inventory marketing-gallery attachments (0.5.0 draft)

The existing vehicle-photo gallery is the only attachment category available in this local draft. An app needs separate `media:read` approval/consent and `inventory:read`; current interactive-user inventory permission is checked as well. Reviewer approval of media access must include inventory read. GET `/api/partners/v1/inventory/{id}/photos` returns at most 25 gallery references (`id`, `vehicleId`, `cover`, and an authenticated relative `download` route). Query-selected fields, storage paths, original filenames and signed URLs are unavailable.

GET `/api/partners/v1/inventory/{id}/photos/{photoId}/download` downloads a no-store JPEG attachment. Both the vehicle and photo must belong to the installed dealership, and the database storage reference must match that exact gallery object. The server reads only the configured protected vehicle-photo bucket; it rejects provider redirects and has a 10-second provider deadline. Input is bounded to 3 MiB and 40 million pixels. JPEG/PNG/WebP raster data is re-encoded at a maximum 1600-pixel dimension, stripping EXIF and other embedded metadata; documents, SVG and unsupported/corrupt formats are rejected. Image processing has a five-second budget excluding thread wait. At most four concurrent photo downloads per runtime instance are allowed; excess work returns 429 without a waiting queue. Distributed capacity/load acceptance remains a launch requirement.

Permissions and the gallery reference are checked again after storage/decoder work. Offboarding, suspension, revoked tokens/families, expired credentials or disconnect deny subsequent downloads. The server does not issue a reusable public URL. Already downloaded bytes cannot be recalled; the app must obey approved purpose, retention and disconnect deletion obligations. Audit entries record successful reference resolution, not proof of network delivery.

This category is for vehicle marketing photos. Technical image validation cannot recognize every sensitive item accidentally placed in a gallery; partners and dealerships must not use it for customer/identity/financial documents, staff information or internal attachments. No generic document, customer, staff or account-photo endpoint exists. Native marketing-gallery storage remains the source; partner upload/replacement/cover changes and all deletion remain unavailable until their complete workflow is implemented and reviewed. Missing/corrupt/noncanonical provider objects return a safe unavailable response without exposing provider details. Local tests use generated images and a local provider fixture; actual protected Supabase storage acceptance, load and failure/recovery exercises remain pending.


## Developer identity, organization access and update submissions

Developer organization access uses a separate identity-provider JWT, not a dealership browser cookie or partner app OAuth token. The backend verifies the configured provider's signature/issuer and live user, confirmed email, authenticated audience/role, nonanonymous identity, active session and TOTP MFA (`aal2`). Sensitive developer mutations require a TOTP confirmation within ten minutes. Token claims or user metadata cannot choose organization roles. Current CarSoft membership and organization status are checked in the database for each operation, including the submission transaction.

A platform administrator records identity/role review evidence before creating an invitation. It is bound to an exact verified email, expires after seven days and consumes once. The administrator receives the token only in the creation response and must share it securely with the reviewed person. POST `{ "invitation": "<one-use value>" }` to `/api/partners/developer/invitations/accept` with the developer identity bearer token. Wrong email, replay, expiry, revocation and an existing membership are rejected. Invitation acceptance grants organization access only. Owner changes require platform review. No production invitation or auth-provider configuration has been activated by this draft.

GET `/api/partners/developer/me` returns your identity and up to 100 active organization memberships. GET `/apps`, `/submissions` and `/members` under that developer API prefix requires `organizationId`, with optional `limit` (1–100) and UUID `after`; follow `pagination.next`. Owner/admin/developer/support can read app/submission manifests; billing members have separate billing authority and no app-submission grant. Team member listings contain developer identifiers, roles/status and epochs, never dealership staff, credentials or customer data. Private platform review evidence is excluded from developer reads.

Owner/admin/developer can POST an app update to `/api/partners/developer/submissions` using `organizationId`, `appId`, the current `baseRevisionId`, a UUID `requestId`, `sourceReference` and the exact proposed `manifest`. Use the same requestId and identical body to recover an original response. A conflicting retry is rejected. Required manifest keys are `description`, `data_use`, `retention_days`, `retention_exceptions`, `pricing`, `platform_charges`, `scopes`, `field_policy`, `embedded_origin`, `locations`, `redirects` and `webhook_origins`. Platform `review_evidence`, approval state, hidden fields and undeclared permissions are rejected. Proposals must satisfy the public field/create requirements; the entire JSON request remains bounded. At most 20 pending proposals per app are allowed; exact retries remain available at the ceiling.

Submissions are immutable and fingerprinted. A separate platform decision may approve, reject or request changes, or record withdrawal. Requested changes require a new submission. Approval publishes the exact saved candidate with fresh functional/security review evidence and current domain proofs. A stale base or fingerprint cannot be approved. A reviewed revision never silently changes existing dealership consent; installation updates still require a dealership administrator.

Owner/admin can PATCH `/api/partners/developer/members` with `organizationId`, `memberId`, current `epoch`, `role`, `status` and `reason`. Admins cannot change owners/peer admins or promote members to admin. Offboarding denies new developer API operations immediately, even while the identity token remains unexpired. Revocation or a role change permanently retires active OAuth clients whose current secret was issued to that member. When a technical member (owner/admin/developer) changes access, older platform-issued clients without a current custody record are also retired conservatively. Clients held only by other known secret holders remain active; an affected shared client must be replaced. Replace revoked clients and obtain fresh dealership authorization; offboarding does not silently reissue credentials or grant consent. Complete self-service signup/login/recovery/MFA management, team-created invitations and isolated sandbox provisioning remain unfinished. Local runtime tests use a signature-validating identity-provider fixture through the installed SDK; actual hosted-provider enrollment/MFA/recovery acceptance remains pending.


## Backend client credential management

GET `/api/partners/developer/credentials` uses `organizationId`, optional UUID `after` and `limit` (1–100). Owner/admin/developer roles see only client IDs, app/revision binding, exact redirects, status, version and creation time. Plaintext secrets, hashes and custody records are never readable.

Owner/admin members with TOTP confirmed within ten minutes can POST `organizationId`, `appId`, `revisionId`, a UUID `requestId` and 1–20 unique `redirects`. The app must be approved in that organization, and the revision/redirects and live domain proof must already be reviewed. The response contains a new clientId, one-time clientSecret and credentialVersion 1. Store the secret in your protected backend secret manager; never put it in a browser, log, URL or repository. At most 20 active clients per app are allowed. Creating a client does not install an app, authorize a dealership, expand scopes or publish a listing.

PATCH the same route with `organizationId`, `clientId`, current `credentialVersion`, `decision` (`rotate` or `revoke`) and `reason`. A concurrent/stale mutation returns 409. Rotation returns a one-time replacement secret, immediately invalidates the previous secret, pending codes and OAuth families, and records the new secret recipient. Revocation is permanent. Rotation requires an active reviewed app/current domain proof; revocation is still allowed while an app is suspended. Never revive old token families after changing credentials; obtain fresh dealer OAuth.

Issuance and rotation record current-secret custody. A secret-changing rotation replaces previous custody, while authorization-only epoch changes (for example domain-proof withdrawal/reverification) preserve custody for the unchanged secret. A membership role/status change revokes exposed current secrets in the same database transaction as offboarding. Untracked older clients are treated as potentially exposed when a technical member changes access. A concurrent rotation by another active owner transfers custody and is rechecked after acquiring the client lock, so the new owner’s secret is preserved.

Credential responses are noncacheable and one-time. Creation stores a stable receipt: an identical requestId/body retry returns the original client ID, current version/status and `secretUnavailable: true` with HTTP 200, never another plaintext secret or another client. Conflicting reuse is rejected, and exact metadata retries work at the active-client ceiling. If the original response/secret was lost, revoke that exact client and deliberately create a replacement using a new requestId. Never guess which client was orphaned or expect plaintext recovery. After a lost rotation response, reread the version and reconcile or deliberately rotate again with the current version; do not blindly replay the old version. Keep separate credentials and authorization transactions for each environment. Complete isolated sandbox provisioning and hosted-provider lifecycle/MFA recovery remain launch requirements; local credential/offboarding tests do not establish production readiness.


## Register and submit your first app

After your organization and identity are manually reviewed, an active owner, admin or developer can POST `/api/partners/developer/apps` with `organizationId`, a trimmed `name` (1–160 characters) and a new UUID `requestId`. A fresh registration returns HTTP 201 with an unpublished draft ID. Registration grants no executable revision, credential, installation or dealership data. An identical request retry returns HTTP 200 with that same app and its current status; changed request reuse returns a conflict. Organizations are limited to 100 registered apps; contact platform support at the ceiling. Billing/support roles cannot register or submit apps.

POST `/api/partners/developer/submissions` with that `appId`, **explicit `baseRevisionId: null`**, your complete proposed manifest, a new `requestId`, `organizationId` and `sourceReference`. The initial candidate moves the draft to review but remains separate from approved executable revisions. Partners cannot submit platform review evidence or an approval state. App registration and submission require provider-verified email, TOTP MFA and a TOTP confirmation within the past ten minutes.

A platform administrator reviews the exact immutable candidate, records functional/security evidence and confirms current exact-host domain ownership. Only that manual approval publishes the first reviewed revision. A failed approval rolls back publication and keeps the proposal pending. Rejection or changes requested grant no access; submit a corrected immutable candidate with a new requestId. Approval creates neither OAuth clients nor dealership installations. Those require separate reviewed credential issuance and dealership consent.

Once a revision is approved, subsequent submissions must use its actual latest revision UUID. Another pending initial proposal with a null base cannot replace it; rebase and submit again. An exact original submission receipt retry still identifies its original candidate after approval. Read current app/review status before continuing. This backend flow does not complete hosted enrollment, team recovery, sandbox provisioning or the full developer portal UI.


## Transfer an organization owner role

An existing owner can request transfer of **their own owner role** to an already active non-owner member of the same organization. This does not remove other reviewed owners. Read the current member identifiers/epochs first. POST `/api/partners/developer/ownership` with `organizationId`, `incomingMemberId`, `outgoingEpoch`, `incomingEpoch`, a new UUID `requestId` and `reason`. Current owner membership and recent TOTP MFA are required. There is one live request per outgoing owner, lasting seven days. Exact request retries recover the original request and its current status/fingerprint; changed reuse conflicts. Creation changes no role, credential or dealership grant.

The incoming member signs in independently, confirms TOTP within ten minutes, reads the exact transfer and PATCHes the same route with `organizationId`, `transferId`, its `fingerprint`, `decision: "accept"` and `reason`. Only that incoming member may accept or decline. Acceptance rechecks both current membership versions and records consent; it does not promote them. Repeated acceptance returns the existing accepted state while the request remains live. The incoming member can use `decision: "decline"`; the outgoing member can use `decision: "cancel"`. Cancellation/decline can close a proposal whose membership versions have since changed, provided the responding person still has active organization membership. An expired/closed request cannot be accepted, edited or approved.

CarSoft's platform administrator then reviews the exact fingerprint, both identities and business authority, with current-password confirmation and private review evidence. Before approval, the server also reads both live identities through the configured provider: missing/deleted/banned users, unverified email or disabled MFA block completion. Provider failure blocks approval and uses a shared six-second deadline. Only that review can complete ownership transfer. A changed membership, withdrawn consent or expired request prevents approval. On approval, the incoming member becomes an owner, the outgoing membership is revoked, and both membership versions advance atomically. Clients whose current secret was issued to the outgoing owner are permanently retired, together with their OAuth sessions; untracked older clients are treated conservatively as exposed. Clients held only by other known people remain active. A shared affected client must be replaced. Other reviewed owners retain their organization membership.

The transfer never creates replacement credentials, dealer authorization or installations, changes dealership consent, or transfers customer funds. Reissue affected credentials deliberately and obtain fresh dealership authorization. GET `/api/partners/developer/ownership` provides organization-scoped paginated history and shared decision notes, excluding private identity/business review evidence. Expiry uses the database clock. Closed history is immutable. Put confidential review details in the platform's evidence field, not shared decision notes.

This is the normal reviewed transfer path. Hosted identity/MFA recovery, business-recovery exceptions and full portal presentation still require acceptance work; an unverified person cannot take ownership through this route.
