# CarSoft partner integration guide

Contract: `partner-openapi.json`, version `0.13.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. Role/status/version changes revoke all pending organization team invitations issued by the member; an accepted teammate membership is not undone. 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, the team portal 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.


## Ordinary team invitations

After platform-reviewed organization onboarding, active owners/admins can manage ordinary team invitations at `/api/partners/developer/invitations`. Owners may invite admin, developer, billing or support roles; admins may invite developer, billing or support. Neither can issue owner invitations or label their own invitation as platform reviewed. Platform bootstrap and owner invitations retain separate provenance and platform control. Team membership grants no app approval, dealership access or new API scopes.

POST `organizationId`, `email`, `role`, a shared `reason` and new UUID `requestId`, using recent provider TOTP MFA. A fresh 201 response includes the one-time `data.invitation` value. Store/share it securely: only its hash is retained. An exact retry returns 200 and the original metadata with `tokenUnavailable: true`; it does not reveal or replace the token. Changed receipt reuse conflicts. Revoke and create a new invitation if the creation response was lost. There are at most 50 pending unexpired invitations per organization, counting platform and team invitations together. No invitation email is sent automatically by this interface.

The recipient uses `/api/partners/developer/invitations/accept` with their own verified-email, recent-MFA identity. The invitation expires after seven days, binds the exact normalized provider email and consumes once. Existing membership cannot be overwritten. For organization invitations, acceptance checks the issuer's live provider identity and current organization role/version; deleted/banned/unverified identities or disabled MFA deny acceptance. Provider failure blocks acceptance with a six-second shared network deadline. The final database operation serializes with issuer offboarding and revocation.

GET with `organizationId`, optional UUID `after` and `limit` from 1–100 returns metadata only, including `review_origin`, issuer member ID, role/email and database-clock pending/accepted/revoked/expired status. Owners/admins may read this history; no token/hash or private evidence is exposed. PATCH `organizationId`, `invitationId` and `reason` with recent MFA revokes an unconsumed organization invitation. Admins cannot revoke admin invitations; platform bootstrap/owner invitations remain platform-controlled. Repeated revocation succeeds. An issuer role/status/version change permanently revokes their pending team invitations, including when an owner transfers their role. Already accepted teammates retain their independently managed memberships.

The hosted team portal, verified-email/MFA enrollment/recovery and provider lifecycle synchronization remain acceptance requirements. The local API and synthetic tests do not establish hosted identity-provider behavior.


## Reviewed sharing of an existing client credential

Prefer separately issued clients for independent services. If another person needs an existing secret, use `/api/partners/developer/sharing` before sharing it. An active owner/admin who is already a recorded holder of that exact current secret can POST `organizationId`, `clientId`, `recipientMemberId`, `issuerEpoch`, `recipientEpoch`, current `credentialVersion`, new UUID `requestId` and shared `reason`, with recent TOTP MFA. The recipient must be another active owner/admin/developer in the same organization. Untracked platform-issued credentials cannot use this workflow: rotate first to establish known custody. No request accepts a secret value.

A request lasts seven days. There is one live request per client/recipient and at most 50 per organization. Exact request retries recover original metadata/status/fingerprint; changed reuse conflicts. GET returns paginated history for technical members, excluding secret hashes and private review evidence. Use `organizationId`, optional UUID `after` and `limit` from 1–100.

The recipient PATCHes `organizationId`, `shareId`, the exact `fingerprint`, `decision: "accept"` and `reason` with recent MFA. Only that recipient may accept/decline; only the issuer may cancel. Acceptance does not grant custody or reveal a secret. CarSoft's platform administrator separately confirms current password, reviews both live identities and custody purpose, and records private review evidence against the exact candidate. Missing/deleted/banned/unverified identities or disabled MFA deny approval; provider failures block approval under a six-second network deadline. SQL atomically rechecks both membership versions, current client version/digest, original issuer custody, approved app/domain proof and expiry. Rotation, offboarding, withdrawn consent or stale versions prevent completion.

After approval, the recipient is recorded as an additional holder of the current secret; existing holders remain recorded. Only then share using your secure delivery channel. CarSoft cannot recover or deliver a hashed secret. If the original creation response was lost, rotate rather than trying to retrieve it. Approval changes no secret, OAuth session, app scopes, dealership installation or dealer consent. Offboarding or changing the role of any recorded current holder permanently retires the shared client and its OAuth families. You cannot undo a person's knowledge by removing custody metadata; rotate/revoke deliberately and obtain fresh dealership authorization. Request history remains immutable after closure, and expired/rotated requests never imply permission to share a future secret.


## CarSoft partner billing agreements

Your organization collects its own app fees. CarSoft separately bills the fees individually agreed with your organization using CarSoft's current Stripe account. No default commercial prices are assumed. An overdue invoice notifies the platform administrator for a decision; it does not automatically suspend your organization or apps.

CarSoft's platform administrator offers exact monthly commercial terms with a 14-day expiry. GET `/api/partners/developer/billing/agreements` with `organizationId`, optional UUID `after` and `limit` from 1 to 100 requires an active owner/admin/billing membership and verified TOTP identity. Returns immutable terms, agreement ID, fingerprint, effective date and current acceptance/decision metadata. Private commercial review evidence is excluded. Technical developer/support roles cannot read commercial terms through this API.

Only an active organization owner may PATCH `organizationId`, `agreementId`, exact `fingerprint`, `decision: "accept"` or `"decline"`, and a shared `reason`. Recent TOTP MFA within ten minutes is required. Offers that expired, were withdrawn or passed their start date cannot be accepted. One accepted agreement per UTC calendar month is allowed. Subsequent prices start on a future calendar month's first day; earlier pending offers are rechecked at acceptance. A same-owner exact acceptance retry returns accepted status without another audit event. Accepted terms cannot be rewritten or withdrawn, and remain historical business agreements after the owner changes.

The exact `terms` object contains currency (lowercase ISO code); integer baseFeeMinor, apiUnitPriceMinor, apiUnitSize, installationFeeMinor, revenueShareBps, spendLimitMinor, dueDays and taxBps; proration (`none` or `daily`); cancellation (`period_end` or `immediate_prorated`); taxMode (`none`, `exclusive` or `inclusive`); fixed pastDuePolicy `manual`; fixed graceDays `0`; and partner-visible description. Monetary prices/limit use the currency's minor units. API usage units and daily fee proration round up. Revenue-share and tax calculations round half up to a minor unit. The monthly spending limit covers all fees before tax. Initial daily agreements may begin partway through a month; full-month agreements start on day one. Commercial/tax responsibilities require explicit review.

Accepting an agreement alone creates no invoice, charge, credential or dealership permission. Automated metering, Stripe invoices/payment reconciliation, billing cancellation and overdue notification delivery remain under implementation. This draft API is locally verified and not activated in Software production.
