# CarSoft partner integration guide

Contract: `partner-openapi.json`, version `0.19.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. Successful-request metering and completed-month calculations are locally implemented. Stripe invoice issuance/payment reconciliation workflows, billing cancellation and overdue notification delivery remain under implementation. This draft API is locally verified and not activated in Software production.


## Completed monthly fee calculations

GET `/api/partners/developer/billing/periods` with `organizationId`, optional UUID `after` and `limit` from 1 to 100 requires an active owner/admin/billing membership and verified TOTP identity. It is read-only. Each immutable record binds the accepted agreement, UTC calendar-month start/end (exclusive), effective start, exact terms, usage, calculation, reviewed revenue statement ID when applicable and fingerprint. Private statement/review evidence is excluded. This record is a fee calculation, not a payment receipt.

Only successful HTTP 2xx production app API requests count as billable API calls. A generated server request ID deduplicates internal recording; concurrent requests retain independent context. OAuth requests, developer account APIs, denied/failed responses and sandbox requests are excluded. Idempotent business-operation retries that successfully return a response are still API calls. Request bodies, credentials and customer records are not stored in the usage ledger.

Installation fees use unique installations and active UTC days. Any positive overlap with a day counts once; reactivation within the same day does not add a second day. Daily agreements prorate installation-day totals; full-month agreements use the unique installed integration count. Tracking begins when the billing ledger is installed; no historical installation activity is invented. The accepted commercial start date excludes earlier usage.

Revenue-share billing requires a platform-reviewed statement for the completed month, exact effective commercial window and agreement currency, including explicit zero revenue. Review verifies the agreed revenue definition, refunds and tax exclusions. Corrections before freezing preserve previous revisions and require the current fingerprint; once frozen, corrections need a separately reviewed adjustment. Missing/mismatched statements block fee preparation rather than assuming revenue is zero. Partner-collected funds remain independent.

The calculation uses ceiling API usage units and daily proration; revenue share and tax round half up to a minor unit. All fee components are capped together before tax, with the excess preserved as a spending credit. Inclusive tax is part of the subtotal; exclusive tax is added. Accepted spending limits plus exclusive tax cannot exceed the invoice safety ceiling. Monetary totals and usage counters are exact strings; avoid converting them to JavaScript Number. Completed-month retries recover the same record and never create another snapshot, invoice or payment receipt.

The local Stripe issuance/reconciliation core uses dedicated partner customers, exact snapshot metadata, stable idempotency keys, draft persistence before finalization, explicit reviewed tax rates, hosted invoice payments and retrieval of confirmed Stripe payment allocations. It has installed-SDK tests against an isolated local protocol fixture, including unknown outcome recovery and deadlines. Automated customer provisioning, worker orchestration, signed billing webhook integration, cancellation/credits and overdue notification delivery are still under implementation; actual Stripe sandbox acceptance and release authorization remain requirements. Overdue policy remains manual platform-admin decision with no automatic suspension.


## CarSoft billing contact consent

`GET /api/partners/developer/billing/account?organizationId=...` returns safe account status and paginated contact consent history to current owner/admin/billing members. Use `after` and a limit of 1–100. Stripe account/customer IDs and private platform review evidence are excluded.

The owner submits `POST /api/partners/developer/billing/account` with exactly `organizationId`, UUID `requestId`, and `consent: true`. This consents to receive CarSoft partner invoices at the owner's provider-verified email. Supply no email, company name, Stripe customer, payment method or app credentials. The server captures the current verified email and reviewed organization name. A verified TOTP challenge within ten minutes is required. The billing role can read history but cannot consent as the owner.

There is one pending contact request per organization, a seven-day expiry and ten new requests per rolling day. Exact UUID retries return the original consent; changed identity/email/epoch under that receipt conflicts. Owner access changes invalidate pending consent. The platform admin reviews the exact request and rechecks the current identity/owner epoch/email before approval. Approval binds the current CarSoft Stripe account and mode, without creating an invoice or granting access.

Customer provisioning is separate, requires release activation and creates a dedicated CarSoft partner billing customer with stable idempotency. It never reuses dealership subscription customers and never attaches a payment method, charges a card, creates Connect accounts or distributes partner funds. Unknown customer creation outcomes older than 23 hours require platform review; a known saved customer can be reverified without creating another. Current owner/identity/epoch checks precede provisioning and persistence. Owner changes require billing contact review before future creation; historical business records remain intact.

These routes, admin review/rejection, SQL constraints and installed Stripe SDK protocol behavior have local isolated checks. Actual Stripe sandbox acceptance, monthly invoice orchestration, payment reconciliation integration, credits/cancellation and overdue notification delivery remain under implementation. Overdue status will notify the platform admin for a manual decision; it will not automatically suspend integrations.


## CarSoft invoice history and overdue decisions

`GET /api/partners/developer/billing/invoices?organizationId=...` requires active owner/admin/billing membership and verified TOTP identity. It accepts UUID `after` and `limit` 1–100. Records expose exact minor-unit strings, immutable month calculations, processing state and last confirmed provider status/time. Stripe account/customer/invoice identifiers and private review evidence are excluded. Hosted links are validated HTTPS `invoice.stripe.com` links. A `review_required` record may show older confirmed amounts; it is not fresh payment evidence.

The platform admin separately queues an invoice from the completed fee period after accepted pricing and reviewed dedicated customer setup. A bounded release-gated worker persists the draft before finalization, uses stable keys tied to the immutable period, validates reviewed tax and customer/account/mode bindings, and verifies payment allocations before recording payments. Unknown outcomes beyond the safe retry window require review. Read-only rechecks can reconcile historical invoices without enabling new invoice creation.

Admin → Partners → Billing decisions shows overdue/provider review notices and accepts password-confirmed decisions to keep access active or request further review. Neither overdue detection nor a billing decision automatically suspends partners. Provider-confirmed paid or void status resolves notices. Monitoring keeps confirmed gross paid, tax and outstanding amounts separate by currency; these figures are not net profit and exclude independent partner-collected fees.

Durable email notifications use CarSoft's existing admin alert address, with one immutable recipient/message intent per notice, leased retries and an accepted-provider receipt. Lost responses retry the identical key within 23 hours; configuration changes, permanent rejection or old unknown outcomes require review. Acceptance does not prove delivery to the inbox. Unsent resolved notices are canceled; an in-flight accepted email may arrive after resolution. Notices remain in the dashboard when email configuration or activation is unavailable. No real emails were sent during the isolated tests.

These workers and admin pages have local production-runtime, installed-SDK, database and browser checks. Automatic monthly scheduling, signed Stripe billing event integration, credits/cancellation/disputes, real provider sandbox acceptance and Software production activation remain unfinished. Native dealership payments remain a separate system. Documentation publication alone does not activate partner API or billing access.


## Automatic monthly billing and signed provider updates

After pricing is accepted and the reviewed billing customer is active, a separate release-gated scheduler discovers the first agreed UTC month. It prepares completed months one at a time, recovers existing manual records and advances only after a durable invoice operation exists. Missing revenue review, exact tax mapping, changed owner/account or security eligibility blocks that month for review without skipping it. Explicit zero revenue requires its own reviewed statement. Current months are not billed in advance. Scheduled calls are authenticated independently from manual worker calls and process bounded work; repeated/concurrent invocation does not create another monthly invoice.

Platform admins can pause or resume/retry monthly preparation with a current password, reason and current schedule version. This does not suspend partner access, forgive owed invoices or stop historical provider reconciliation. Tax mappings are reviewed per accepted agreement and billing account; the Stripe account, active rate, mode, inclusion and exact percentage must match. Operational monitoring links blocked schedules to their required review. Hosted schedule activation, sufficient deployment-plan capacity and release approval remain outstanding.

Native Stripe webhook signatures use the original raw request bytes, with bounded size/time limits. Previously persisted provider invoice IDs determine ownership, not caller/event metadata. The handler retrieves current Stripe state and verified payment allocations, so duplicate/out-of-order delivery and expired leases cannot fabricate payment or regress a confirmed paid record. Connected-account and mode-mismatched events are rejected. Reconciliation works separately from creation activation; it cannot create, finalize or send an invoice. Other native events keep their existing handler. Polling remains available for recovery.

Void reconciliation preserves the frozen calculation but requires zero collectible, paid and remaining amounts, without adding revenue; a previously confirmed paid invoice cannot regress to void. Actual processor acceptance, cancellation/termination/credits/refunds/disputes, admin MFA and the complete developer self-service portal remain unfinished. Documentation publication does not activate these Software features.


### Voluntary cancellation (0.18)

The organization owner can confirm cancellation using GET/POST /api/partners/developer/billing/cancellation. Read the current agreement first, supply its exact fingerprint, one requestId, a reason and confirmation=cancel. Recent TOTP MFA is mandatory; billing/admin roles cannot cancel. Retrying the same intent recovers the same immutable record.

The accepted agreement defines the date: period_end uses the next UTC month boundary; immediate_prorated stops new app/API access immediately and counts the current UTC day in the final daily fee calculation. Guards use the database clock and apply to existing tokens, grant creation, installations, event delivery and new processor submissions. Existing submitted payments still reconcile. Canceled apps leave the dealership gallery and cannot open inside CarSoft. Scheduled cleanup disconnects installations, revokes interactive grants and preserves deletion_due_at=original access cutoff+30 days even if cleanup runs late. No new fees accrue beyond the agreed commercial window; the final completed-month calculation retains the full calendar identity and actual active days. Future offers cannot silently extend cancellation.

Already owed invoices remain payable. Active owner/admin/billing members can still discover the organization and read agreements, periods and invoices after cancellation. Revoked memberships remain denied. Overdue billing always awaits the platform administrator's decision and never automatically cancels or suspends a partner. Reinstatement is not exposed through this cancellation endpoint and needs separate platform review. Platform admins can monitor cancellation dates and the recorded reason in /admin/partners/billing/{organizationId}/cancellation.

Actual partner fee billing acceptance used the existing Stripe sandbox with synthetic customers only. No-tax paid, exclusive-tax void, inclusive-tax paid and zero-fee paid invoices passed with the installed SDK. Stripe retains original due/remaining fields on void invoices; CarSoft verifies them and reports zero collectible remaining. Stripe API invoice sends in test mode send no email. This evidence covers partner fee billing, not every dealership merchant adapter, actual email delivery or production activation.

### Platform review protection and billing operations

Sensitive platform-admin partner approvals, permissions, credentials and billing decisions require the current native password and a verified authenticator session. Setup, replacement, one-use recovery and security activity are managed in Partner security. Verification lasts ten minutes and binds to the exact signed-in session; partner identities continue to use their separately verified developer MFA.

A reviewed Stripe tax-rate replacement must match the accepted percentage, tax inclusion, account and mode. Review history is retained and already queued invoices keep their original rate. A replacement does not change an accepted price or rewrite past bills. Overdue access decisions remain manual.

Admin notification recovery never invents email delivery. Within the safe 23-hour window an operator may retry the same immutable message and idempotency key. Older uncertain outcomes require a matching provider receipt, verified through a read-only lookup; a foreign recipient, altered message or failed provider state is rejected. These local controls still await Software release and live notification acceptance. Actual Stripe sandbox evidence remains scoped to CarSoft partner fee billing.

### Merchant adapter verification update

The marketplace Stripe adapter now verifies its actual merchant account and test/live mode before effects and reconciliation. Saved methods must belong to the confirmed customer. Native payment metadata binds the provider transaction to the exact CarSoft intent, dealership and invoice; refunds additionally verify the original confirmed source payment. Stable keys and request bodies prevent replacement effects across supported retries. Native link creation outside its creation window requires review.

Actual synthetic sandbox acceptance passed wrong-account/mode/customer refusal, one saved-method charge, one bound refund and one stable hosted link subsequently expired, using the installed adapter SDK. The entire synthetic charge was refunded. This adds merchant SDK evidence to the earlier partner-fee billing evidence. Full hosted merchant/customer setup, API-to-native-ledger acceptance against that account, completed hosted checkout and external processor acceptance remain unfinished. No live financial action occurred.


### Developer portal and current session verification update

A local Software developer portal foundation now covers sign-in, email-code confirmation, authenticator setup/verification, password recovery that preserves MFA, reviewed organization/application/team/credential/billing views, and the existing sensitive-action APIs. Its cookie contains only a random opaque handle; provider credentials are encrypted server-side. Local sessions have a fixed 12-hour lifetime and serialize refresh/factor mutations. Unknown refresh outcomes require fresh sign-in; they are not replayed. Developer APIs also verify the current provider session, and service-only database requests recheck it within the admitted transaction. Local retirement immediately denies CarSoft access even when upstream sign-out is unavailable. Password/email recovery alone never supplies organization-level MFA or creates membership.

Real disposable Supabase Auth plus the actual Software browser/database pipeline passed sign-in, wrong-authenticator rejection, real verification, draft creation, cross-organization denial, mobile/desktop geometry and durable provider/local sign-out. This is isolated local acceptance, not hosted enrollment, CAPTCHA, email delivery or production release. The portal is gated by `CARSOFT_PARTNER_PORTAL_ENABLED` as well as the marketplace gate. Before public activation, configure confirmed-email enrollment and code-bearing confirmation/recovery email templates, existing hostname-bound Turnstile, provider lifecycle/recovery review and the isolated sandbox; verify them separately. Remaining portal controls and the overall Software launch remain active work. Public Website application intake remains disabled pending coordinated release.


### Developer organization management update

The local gated portal now includes ordinary team invitation creation/revocation, ownership transfer requests/cancellation and incoming responses, and credential-sharing requests/cancellation and recipient responses. Ownership and credential sharing require recent verified TOTP, consent from the incoming member/recipient, and separate CarSoft platform review. Acceptance alone grants no secret or expanded authority. One-use invitations remain bound to the verified email; share them privately. No invitation email is sent by this flow.

Organization owners/admins can create a domain TXT challenge through `POST /api/partners/developer/domains` with `organizationId` and an exact public `hostname`. The response returns the proof `id`, `version`, one-time DNS `record` and `value`, and a one-hour expiration. Add that TXT record to DNS, then `PATCH` the same endpoint with `organizationId`, `proofId`, `version`, and `decision: "verify"`. Verification lasts 30 days. A rotated challenge replaces an earlier unfinished challenge; create a fresh one if a response was lost. Renewing a challenge preserves current verified ownership until expiry or withdrawal.

`GET /api/partners/developer/domains?organizationId=...` is paginated for current owner/admin/developer members and exposes metadata only. `PATCH` with `decision: "revoke"` and a nonempty `reason` withdraws the current version. Domain administration requires recent TOTP and a current owner/admin membership. It is organization-scoped and limited to 100 records; application-owned intake proofs remain platform administered. Domain proof does not approve an application or grant dealership authority. Expiry/withdrawal prevents new access; reverification cannot revive old OAuth tokens. DNS is bounded and runs outside database transactions; membership, live provider session and exact challenge/version are rechecked transactionally afterward.

Portal form mutations use a fixed JSON dispatch with exact-origin checking, bounded request bodies and a shared absolute request deadline. Native dealership/admin cookies, posted actor values, caller-selected upstream endpoints and reserved object-field names cannot confer developer authority. Hosted enrollment email/CAPTCHA and reviewed lost-authenticator recovery remain pending; these local controls are not an availability or full-launch claim.


### Authenticator replacement

The local portal security page can set up a replacement while one current TOTP authenticator remains verified. Starting replacement requires recent TOTP; only one unfinished enrollment is allowed. Verify the new authenticator before removing your old one. Removing a verified factor requires another verified TOTP factor, recent TOTP and ownership of the selected factor. It signs out all provider sessions, retires the opaque local session, and requires a fresh password/authenticator sign-in. The last verified authenticator cannot be removed through this flow. Replacement start and removal are audited by factor ID without setup secrets or codes. Password recovery alone does not bypass TOTP; reviewed lost-authenticator recovery remains separate unfinished work.

This behavior was checked against the installed SDK and real disposable Supabase Auth: last-factor removal was refused, the new factor was enrolled and verified, the old factor was removed, all actual provider sessions ended, and a new sign-in with the remaining factor succeeded. Hosted account configuration and email/CAPTCHA delivery remain pending. Provider API references: [enrollment](https://supabase.com/docs/reference/javascript/auth-mfa-enroll) and [unenrollment](https://supabase.com/docs/reference/javascript/auth-mfa-unenroll).
