openapi: 3.1.0 info: title: OSG Account Server API version: 0.1.0 description: | Account, credit, referral, integrity, and managed AI APIs for OSGKeyboard. Local features and user-owned API keys remain independent of this service. servers: - url: https://account.osglab.com security: - bearerAuth: [] paths: /health: get: security: [] summary: Compatibility health check responses: "200": { $ref: "#/components/responses/HealthUp" } /health/live: get: security: [] summary: Process liveness responses: "200": { $ref: "#/components/responses/HealthUp" } /health/ready: get: security: [] summary: Database and migration readiness responses: "200": { $ref: "#/components/responses/HealthUp" } "503": description: Database is unavailable or migrations failed. /v1/auth/apple: post: security: [] summary: Exchange Sign in with Apple credentials for an OSG session requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/AppleSignInRequest" } responses: "200": description: Authenticated session content: application/json: schema: { $ref: "#/components/schemas/SessionTokenEnvelope" } default: { $ref: "#/components/responses/Error" } /v1/auth/refresh: post: security: [] summary: Rotate a refresh token requestBody: required: true content: application/json: schema: type: object additionalProperties: false required: [refreshToken] properties: refreshToken: { type: string, minLength: 32 } responses: "200": description: Rotated session content: application/json: schema: { $ref: "#/components/schemas/SessionTokenEnvelope" } default: { $ref: "#/components/responses/Error" } /v1/auth/logout: post: summary: Revoke the current session family responses: "204": { description: Session revoked } default: { $ref: "#/components/responses/Error" } /v1/account: get: summary: Return the minimal account profile responses: "200": description: Account profile content: application/json: schema: { $ref: "#/components/schemas/AccountEnvelope" } default: { $ref: "#/components/responses/Error" } delete: summary: Reauthenticate with Apple, delete the account, and revoke authorization requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/DeleteAccountRequest" } responses: "204": { description: Account deleted } default: { $ref: "#/components/responses/Error" } /v1/apple/events: post: security: [] summary: Receive a signed Apple server-to-server account event requestBody: required: true content: application/json: schema: type: object additionalProperties: false required: [payload] properties: payload: { type: string } responses: "204": { description: Event accepted } default: { $ref: "#/components/responses/Error" } /v1/credits/balance: get: summary: Return available integer credits responses: "200": description: Credit account content: application/json: schema: { $ref: "#/components/schemas/CreditAccount" } default: { $ref: "#/components/responses/Error" } /v1/credits/ledger: get: summary: Return immutable credit history parameters: - $ref: "#/components/parameters/Limit" responses: "200": description: Ledger entries content: application/json: schema: type: array items: { $ref: "#/components/schemas/LedgerEntry" } default: { $ref: "#/components/responses/Error" } /v1/credits/rates: get: summary: Return effective managed-provider rate cards responses: "200": description: Effective rate cards content: application/json: schema: type: array items: { type: object, additionalProperties: true } default: { $ref: "#/components/responses/Error" } /v1/referrals: get: summary: List invitees without exposing their Apple identity parameters: - $ref: "#/components/parameters/Limit" responses: "200": description: Referral bindings content: application/json: schema: type: array items: { type: object, additionalProperties: true } default: { $ref: "#/components/responses/Error" } /v1/referrals/me: get: summary: Return the current referral code and binding responses: "200": description: Referral profile content: application/json: schema: { type: object, additionalProperties: true } default: { $ref: "#/components/responses/Error" } /v1/referrals/code: post: summary: Idempotently create the current campaign invite code responses: "200": description: Invite code content: application/json: schema: { $ref: "#/components/schemas/ReferralCode" } default: { $ref: "#/components/responses/Error" } /v1/referrals/redeem: post: summary: Bind the account to an inviter during the eligibility window requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/RedeemReferralRequest" } responses: "200": { description: Referral binding } default: { $ref: "#/components/responses/Error" } /v1/referrals/bind: post: deprecated: true summary: Compatibility alias for referral redemption requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/RedeemReferralRequest" } responses: "200": { description: Referral binding } default: { $ref: "#/components/responses/Error" } /v1/referrals/campaigns: get: summary: Return active referral campaigns responses: "200": { description: Active campaigns } default: { $ref: "#/components/responses/Error" } /v1/integrity/challenges: post: security: [] summary: Issue a single-use App Attest challenge requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/AppAttestChallengeRequest" } responses: "201": description: Challenge issued content: application/json: schema: { $ref: "#/components/schemas/AppAttestChallengeResponse" } default: { $ref: "#/components/responses/Error" } /v1/integrity/attest: post: security: [] summary: Register a validated App Attest key requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/AppAttestationRequest" } responses: "204": { description: Key registered } default: { $ref: "#/components/responses/Error" } /v1/integrity/assert: post: security: [] summary: Validate a challenge-only App Attest assertion requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/AppAssertionRequest" } responses: "200": { description: Assertion counter advanced } default: { $ref: "#/components/responses/Error" } /v1/gateway/catalog: get: summary: Return configured managed-provider capabilities responses: "200": { description: Provider catalog } default: { $ref: "#/components/responses/Error" } /v1/gateway/grants: post: summary: Create an idempotent managed-provider grant parameters: - $ref: "#/components/parameters/IdempotencyKey" requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/CreateGatewayGrantRequest" } responses: "201": description: Gateway grant and rotating credentials content: application/json: schema: { $ref: "#/components/schemas/GatewayGrantTokens" } default: { $ref: "#/components/responses/GatewayError" } /v1/gateway/grants/refresh: post: security: [] summary: Rotate a gateway refresh token parameters: - $ref: "#/components/parameters/IdempotencyKey" requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/RefreshGatewayGrantRequest" } responses: "200": description: Rotated gateway credentials content: application/json: schema: { $ref: "#/components/schemas/GatewayGrantTokens" } default: { $ref: "#/components/responses/GatewayError" } /v1/gateway/grants/{grantId}: delete: summary: Revoke a gateway grant parameters: - name: grantId in: path required: true schema: { type: string, format: uuid } responses: "204": { description: Gateway grant revoked } default: { $ref: "#/components/responses/GatewayError" } /v1/gateway/llm/{capability}: post: summary: Run a metered polish, AI, or agent request parameters: - $ref: "#/components/parameters/RequestId" - name: capability in: path required: true schema: { type: string, enum: [polish, ai, agent] } requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/TextGatewayRequest" } responses: "200": description: JSON response, or SSE when stream is true default: { $ref: "#/components/responses/GatewayError" } /v1/gateway/asr: post: summary: Run buffered metered ASR parameters: - $ref: "#/components/parameters/RequestId" - { name: X-Audio-Duration-Ms, in: header, required: true, schema: { type: integer, minimum: 1, maximum: 600000 } } - { name: X-Audio-Format, in: header, schema: { type: string, enum: [pcm, wav, ogg, mp3], default: pcm } } - { name: X-Audio-Codec, in: header, schema: { type: string, enum: [raw, opus], default: raw } } requestBody: required: true content: application/octet-stream: schema: { type: string, contentEncoding: binary } responses: "200": { description: Newline-delimited Volcengine ASR result frames } default: { $ref: "#/components/responses/GatewayError" } /v1/gateway/asr/sessions: post: summary: Reserve credits and create a one-shot streaming ASR session parameters: - $ref: "#/components/parameters/RequestId" requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/CreateAsrSessionRequest" } responses: "201": description: Session created content: application/json: schema: { $ref: "#/components/schemas/CreateAsrSessionResponse" } default: { $ref: "#/components/responses/GatewayError" } /v1/gateway/asr/sessions/{sessionId}/stream: get: summary: Upgrade to WebSocket and stream binary audio frames description: | Send authenticated binary audio frames, then the exact text frame `{"type":"end"}`. The server forwards binary provider-result frames and closes the one-shot session after settlement. parameters: - name: sessionId in: path required: true schema: { type: string, format: uuid } responses: "101": { description: WebSocket upgrade } default: { $ref: "#/components/responses/GatewayError" } /.well-known/apple-app-site-association: get: servers: - url: https://osglab.com security: [] summary: Return the Apple Universal Links association document responses: "200": description: AASA JSON without a redirect headers: Cache-Control: schema: { type: string, const: "no-store, max-age=0" } content: application/json: schema: { $ref: "#/components/schemas/AppleAppSiteAssociation" } /apple-app-site-association: get: servers: - url: https://osglab.com security: [] summary: Return the root compatibility AASA document responses: "200": description: AASA JSON without a redirect headers: Cache-Control: schema: { type: string, const: "no-store, max-age=0" } content: application/json: schema: { $ref: "#/components/schemas/AppleAppSiteAssociation" } /i/{code}: get: servers: - url: https://osglab.com security: [] summary: Privacy-preserving invitation landing page parameters: - name: code in: path required: true schema: { type: string, pattern: "^[A-Za-z0-9_-]{22}$" } responses: "200": { description: Bilingual HTML landing page } "404": { description: Invalid, unknown, or expired invitation } "503": { description: Invitation lookup is temporarily unavailable } components: securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: JWT parameters: Limit: name: limit in: query schema: { type: integer, minimum: 1, maximum: 100, default: 50 } RequestId: name: X-Request-ID in: header required: true schema: { type: string, pattern: "^[A-Za-z0-9_-]{8,64}$" } IdempotencyKey: name: Idempotency-Key in: header required: true schema: { type: string, minLength: 1, maxLength: 255 } responses: HealthUp: description: Service is healthy content: application/json: schema: type: object required: [status] properties: status: { type: string, const: UP } Error: description: API error content: application/json: schema: { $ref: "#/components/schemas/ApiErrorEnvelope" } GatewayError: description: Managed-provider error content: application/json: schema: { $ref: "#/components/schemas/GatewayError" } schemas: AppleSignInRequest: type: object additionalProperties: false required: [identityToken, authorizationCode, nonce] properties: identityToken: { type: string } authorizationCode: { type: string } nonce: { type: string } deviceCheckToken: type: ["string", "null"] description: Ephemeral DeviceCheck token; never persisted in plaintext. appAttest: oneOf: - $ref: "#/components/schemas/AppAttestAssertion" - type: "null" AppAttestAssertion: type: object additionalProperties: false required: [keyId, challengeId, challenge, assertion] properties: keyId: { type: string, maxLength: 128 } challengeId: { type: string, format: uuid } challenge: { type: string, description: Base64URL challenge returned by the server } assertion: { type: string, contentEncoding: base64 } SessionTokenResponse: type: object required: [accountId, tokenType, accessToken, accessTokenExpiresAtEpochSeconds, refreshToken, refreshTokenExpiresAtEpochSeconds] properties: accountId: { type: string, format: uuid } tokenType: { type: string, const: Bearer } accessToken: { type: string } accessTokenExpiresAtEpochSeconds: { type: integer, format: int64 } refreshToken: { type: string } refreshTokenExpiresAtEpochSeconds: { type: integer, format: int64 } SessionTokenEnvelope: type: object additionalProperties: false required: [data] properties: data: { $ref: "#/components/schemas/SessionTokenResponse" } Account: type: object required: [id, createdAtEpochSeconds] properties: id: { type: string, format: uuid } createdAtEpochSeconds: { type: integer, format: int64 } AccountEnvelope: type: object additionalProperties: false required: [data] properties: data: { $ref: "#/components/schemas/Account" } DeleteAccountRequest: type: object additionalProperties: false required: [identityToken, authorizationCode, nonce] properties: identityToken: { type: string, maxLength: 16384 } authorizationCode: { type: string, maxLength: 4096 } nonce: { type: string, maxLength: 512 } CreditAccount: type: object additionalProperties: true required: [userId, balance] properties: userId: { type: string, format: uuid } balance: { type: integer, format: int64, minimum: 0 } LedgerEntry: type: object additionalProperties: true required: [id, amountDelta, balanceAfter] properties: id: { type: string, format: uuid } amountDelta: { type: integer, format: int64 } balanceAfter: { type: integer, format: int64, minimum: 0 } AppleAppSiteAssociation: type: object additionalProperties: false required: [applinks] properties: applinks: type: object additionalProperties: false required: [details] properties: details: type: array items: type: object required: [appIDs, components] properties: appIDs: type: array items: { type: string } components: type: array items: { type: object, additionalProperties: true } ReferralCode: type: object additionalProperties: true properties: code: { type: string, pattern: "^[A-Za-z0-9_-]{22}$" } RedeemReferralRequest: type: object additionalProperties: false required: [code] properties: code: { type: string, pattern: "^[A-Za-z0-9_-]{22}$" } AppAttestChallengeRequest: type: object additionalProperties: false required: [purpose, keyId] properties: purpose: { type: string, enum: [attestation, assertion] } keyId: { type: string, maxLength: 128 } AppAttestChallengeResponse: type: object required: [challengeId, challenge, expiresAtEpochSeconds] properties: challengeId: { type: string, format: uuid } challenge: { type: string } expiresAtEpochSeconds: { type: integer, format: int64 } AppAttestationRequest: type: object additionalProperties: false required: [challengeId, challenge, keyId, attestationObject] properties: challengeId: { type: string, format: uuid } challenge: { type: string } keyId: { type: string } attestationObject: { type: string, contentEncoding: base64 } AppAssertionRequest: type: object additionalProperties: false required: [challengeId, challenge, keyId, assertion, clientDataHash] properties: challengeId: { type: string, format: uuid } challenge: { type: string } keyId: { type: string } assertion: { type: string, contentEncoding: base64 } clientDataHash: { type: string, description: Base64URL SHA-256 of the echoed challenge } TextGatewayRequest: type: object additionalProperties: false required: [input] properties: input: { type: string, minLength: 1, maxLength: 32000 } context: { type: ["string", "null"], maxLength: 32000 } maxOutputTokens: { type: integer, minimum: 1, maximum: 4096, default: 512 } temperature: { type: number, minimum: 0, maximum: 1, default: 0.2 } stream: { type: boolean, default: false } CreateGatewayGrantRequest: type: object additionalProperties: false required: [scopes] properties: scopes: type: array uniqueItems: true minItems: 1 items: { type: string, enum: [polish, ai, agent, asr] } lifetimeSeconds: { type: ["integer", "null"], format: int64, minimum: 1 } RefreshGatewayGrantRequest: type: object additionalProperties: false required: [refreshToken] properties: refreshToken: { type: string, minLength: 32 } GatewayGrantTokens: type: object additionalProperties: false required: [grantId, scopes, accessToken, accessExpiresAt, refreshToken, refreshExpiresAt] properties: grantId: { type: string, format: uuid } scopes: type: array uniqueItems: true items: { type: string, enum: [polish, ai, agent, asr] } accessToken: { type: string } accessExpiresAt: { type: string, format: date-time } refreshToken: { type: string } refreshExpiresAt: { type: string, format: date-time } CreateAsrSessionRequest: type: object additionalProperties: false required: [estimatedDurationMillis] properties: format: { type: string, enum: [pcm, wav, ogg, mp3], default: pcm } codec: { type: string, enum: [raw, opus], default: raw } sampleRate: { type: integer, const: 16000 } bits: { type: integer, const: 16 } channels: { type: integer, minimum: 1, maximum: 2, default: 1 } language: { type: ["string", "null"], maxLength: 32 } estimatedDurationMillis: { type: integer, minimum: 1, maximum: 600000 } CreateAsrSessionResponse: type: object required: [sessionId, websocketPath, maxFrameBytes, idleTimeoutMillis] properties: sessionId: { type: string, format: uuid } websocketPath: { type: string } maxFrameBytes: { type: integer } idleTimeoutMillis: { type: integer, format: int64 } ApiError: type: object additionalProperties: false required: [code, message] properties: code: { type: string } message: { type: string } ApiErrorEnvelope: type: object additionalProperties: false required: [error] properties: error: { $ref: "#/components/schemas/ApiError" } GatewayError: type: object required: [code, message, requestId] properties: code: { type: string } message: { type: string } requestId: { type: string }