openapi: 3.1.0 info: title: VACT API version: "1.0.0" description: > VACT is a WebRTC calling platform. Your **backend** mints a one-time access token with your App Secret; your **client** exchanges it for a session token and then places or receives calls. The published SDKs (`vact_sdk`, `@firstlogicmetalab/client`, `@firstlogicmetalab/server-sdk`, `vact-server`, `firstlogicmetalab/vact-server`) wrap this protocol — this spec documents the raw HTTP contract underneath them. contact: name: VACT url: https://vact.online servers: - url: https://vact.online description: Production tags: - name: Backend description: Called from your server with your App Secret. Never from a client. - name: Client description: Called from the end-user's device with a session token. - name: Meta description: Unauthenticated service endpoints. paths: /v1/health: get: tags: [Meta] summary: Liveness probe security: [] responses: "200": description: Service is up. content: application/json: schema: type: object properties: ok: { type: boolean, example: true } api: { type: string, example: vact } version: { type: string, example: v1 } /v1/apps/{appId}/tokens: post: tags: [Backend] summary: Mint a one-time access token for one of your users description: > Call this from your backend after your own authentication has decided who the user is. Returns a short-lived `accessToken` (`vact_at_…`, valid 5 minutes) that the client exchanges once. This is the only endpoint most integrations call directly. operationId: issueAccessToken security: - appSecret: [] parameters: - $ref: '#/components/parameters/AppId' requestBody: required: true content: application/json: schema: type: object required: [userId] properties: userId: type: string maxLength: 64 pattern: '^[A-Za-z0-9_.-]{1,64}$' description: Your own stable id for this user. example: alice permissions: type: array items: { type: string } description: Optional narrowing of what the session may do. allowedCalleeIds: type: array items: { type: string } maxItems: 50 description: If set, the session may only call these user ids. sessionTtlSeconds: type: integer minimum: 300 maximum: 86400 default: 3600 description: How long the resulting session stays valid. responses: "201": description: Token minted. content: application/json: schema: type: object properties: appId: { type: string } userId: { type: string } accessToken: type: string description: One-time token; hand to the client, valid 5 minutes. example: vact_at_9f8c… tokenExpiresAt: { type: string, format: date-time } sessionExpiresAt: { type: string, format: date-time } permissions: { type: array, items: { type: string } } allowedCalleeIds: type: [array, "null"] items: { type: string } "400": $ref: '#/components/responses/BadRequest' "401": $ref: '#/components/responses/Unauthorized' "403": description: App ID / App Secret rejected. content: application/json: schema: { $ref: '#/components/schemas/Error' } example: error: { code: invalid_app_credentials, message: invalid App ID or App Secret } "429": $ref: '#/components/responses/RateLimited' /v1/apps/{appId}/webhook: put: tags: [Backend] summary: Set the webhook endpoint for call events operationId: configureWebhook security: - appSecret: [] parameters: - $ref: '#/components/parameters/AppId' requestBody: required: true content: application/json: schema: type: object properties: url: type: string format: uri description: HTTPS endpoint that will receive signed event POSTs. responses: "200": { description: Webhook saved. } "401": { $ref: '#/components/responses/Unauthorized' } "403": { $ref: '#/components/responses/Forbidden' } delete: tags: [Backend] summary: Remove the configured webhook endpoint operationId: deleteWebhook security: - appSecret: [] parameters: - $ref: '#/components/parameters/AppId' responses: "200": { description: Webhook removed. } "401": { $ref: '#/components/responses/Unauthorized' } /v1/apps/{appId}/webhook-deliveries: get: tags: [Backend] summary: List recent webhook delivery attempts operationId: listWebhookDeliveries security: - appSecret: [] parameters: - $ref: '#/components/parameters/AppId' responses: "200": description: Recent deliveries, newest first. content: application/json: schema: type: object properties: deliveries: type: array items: type: object properties: event: { type: string } status: { type: integer } deliveredAt: { type: string, format: date-time } "401": { $ref: '#/components/responses/Unauthorized' } /v1/apps/{appId}/users/{userId}: delete: tags: [Backend] summary: Delete all VACT data for one user (GDPR erasure) operationId: deleteAppUserData security: - appSecret: [] parameters: - $ref: '#/components/parameters/AppId' - name: userId in: path required: true schema: { type: string, pattern: '^[A-Za-z0-9_.-]{1,64}$' } responses: "200": { description: User data deleted. } "401": { $ref: '#/components/responses/Unauthorized' } /v1/apps/{appId}/calls/{callId}/terminate: post: tags: [Backend] summary: Force-terminate a call from your server operationId: terminateCall security: - appSecret: [] parameters: - $ref: '#/components/parameters/AppId' - $ref: '#/components/parameters/CallId' responses: "200": { description: Call terminated. } "401": { $ref: '#/components/responses/Unauthorized' } "404": { $ref: '#/components/responses/NotFound' } /v1/session/exchange: post: tags: [Client] summary: Exchange a one-time access token for a session token description: > The first call a client makes. Trades the `accessToken` from your backend for a `sessionToken` (`vact_st_…`) used as the Bearer credential on every later client call. An access token can be exchanged only once. operationId: exchangeSession security: [] requestBody: required: true content: application/json: schema: type: object required: [appId, accessToken] properties: appId: { type: string, example: vact_app_… } accessToken: { type: string, example: vact_at_… } responses: "200": description: Session established. content: application/json: schema: type: object properties: sessionToken: type: string description: Bearer credential for all Client endpoints. example: vact_st_… userId: { type: string } uid: { type: string } appId: { type: string } sessionExpiresAt: { type: string, format: date-time } customToken: type: [string, "null"] description: Deprecated; only for clients still signing in to Firebase. "400": { $ref: '#/components/responses/BadRequest' } "401": description: Access token invalid, expired, or already exchanged. content: application/json: schema: { $ref: '#/components/schemas/Error' } /v1/events: get: tags: [Client] summary: Long-poll for incoming calls and signalling events description: Held open until an event is ready or the poll window elapses. operationId: getEvents security: - sessionToken: [] responses: "200": description: Zero or more events. content: application/json: schema: type: object properties: events: type: array items: { type: object } "401": { $ref: '#/components/responses/Unauthorized' } /v1/rtc/config: get: tags: [Client] summary: Fetch ICE/TURN configuration for the session operationId: getRtcConfig security: - sessionToken: [] responses: "200": description: ICE server list. content: application/json: schema: type: object properties: iceServers: type: array items: { type: object } "401": { $ref: '#/components/responses/Unauthorized' } "429": { $ref: '#/components/responses/RateLimited' } /v1/calls: post: tags: [Client] summary: Place a call operationId: createCall security: - sessionToken: [] parameters: - name: Idempotency-Key in: header required: false schema: { type: string, pattern: '^[A-Za-z0-9_.:-]{8,128}$' } requestBody: required: true content: application/json: schema: type: object required: [toUserId, offer] properties: toUserId: { type: string, pattern: '^[A-Za-z0-9_.-]{1,64}$' } callType: { type: string, enum: [audio, video], default: audio } offer: type: object description: WebRTC session description (SDP offer). callerName: { type: string, maxLength: 100 } responses: "201": description: Call created and ringing. content: application/json: schema: type: object properties: callId: { type: string, example: call_… } token: { type: string, description: Echo of callId, for older clients. } status: { type: string, example: ringing } createdAt: { type: string, format: date-time } expiresAt: { type: string, format: date-time } purgeAt: { type: string, format: date-time } "400": { $ref: '#/components/responses/BadRequest' } "402": description: Calling suspended for an unpaid bill. content: application/json: schema: { $ref: '#/components/schemas/Error' } "403": description: Callee not permitted for this session. content: application/json: schema: { $ref: '#/components/schemas/Error' } /v1/calls/{callId}/rtc: get: tags: [Client] summary: Call-scoped TURN credentials operationId: getCallRtcConfig security: - sessionToken: [] parameters: - $ref: '#/components/parameters/CallId' responses: "200": { description: ICE servers scoped to this call. } "401": { $ref: '#/components/responses/Unauthorized' } /v1/calls/{callId}/candidates: post: tags: [Client] summary: Submit ICE candidates for a call operationId: submitCandidates security: - sessionToken: [] parameters: - $ref: '#/components/parameters/CallId' requestBody: required: true content: application/json: schema: type: object properties: candidates: type: array items: { type: object } responses: "200": { description: Candidates accepted. } "401": { $ref: '#/components/responses/Unauthorized' } /v1/calls/{callId}/restart: post: tags: [Client] summary: ICE-restart a call after a network change operationId: restartCall security: - sessionToken: [] parameters: - $ref: '#/components/parameters/CallId' responses: "200": { description: Restart accepted. } "401": { $ref: '#/components/responses/Unauthorized' } /v1/calls/{callId}/accept: post: tags: [Client] summary: Accept an incoming call operationId: acceptCall security: [ { sessionToken: [] } ] parameters: [ { $ref: '#/components/parameters/CallId' } ] requestBody: required: true content: application/json: schema: type: object properties: answer: type: object description: WebRTC answer (SDP). responses: "200": { description: Call accepted. } "401": { $ref: '#/components/responses/Unauthorized' } /v1/calls/{callId}/connected: post: tags: [Client] summary: Mark the media path as connected operationId: markCallConnected security: [ { sessionToken: [] } ] parameters: [ { $ref: '#/components/parameters/CallId' } ] responses: "200": { description: Acknowledged. } "401": { $ref: '#/components/responses/Unauthorized' } /v1/calls/{callId}/heartbeat: post: tags: [Client] summary: Keep an active call alive description: Sent periodically; a call whose heartbeat lapses is reaped. operationId: heartbeatCall security: [ { sessionToken: [] } ] parameters: [ { $ref: '#/components/parameters/CallId' } ] responses: "200": { description: Acknowledged. } "401": { $ref: '#/components/responses/Unauthorized' } /v1/calls/{callId}/decline: post: tags: [Client] summary: Decline an incoming call operationId: declineCall security: [ { sessionToken: [] } ] parameters: [ { $ref: '#/components/parameters/CallId' } ] responses: "200": { description: Declined. } "401": { $ref: '#/components/responses/Unauthorized' } /v1/calls/{callId}/cancel: post: tags: [Client] summary: Cancel an outgoing call before it is answered operationId: cancelCall security: [ { sessionToken: [] } ] parameters: [ { $ref: '#/components/parameters/CallId' } ] responses: "200": { description: Cancelled. } "401": { $ref: '#/components/responses/Unauthorized' } /v1/calls/{callId}/end: post: tags: [Client] summary: End an active call operationId: endCall security: [ { sessionToken: [] } ] parameters: [ { $ref: '#/components/parameters/CallId' } ] responses: "200": { description: Ended. } "401": { $ref: '#/components/responses/Unauthorized' } components: parameters: AppId: name: appId in: path required: true description: Your public App ID. schema: type: string pattern: '^(?:vact_app_[a-f0-9]{24}|cp_app_[a-f0-9]{24}|app_[a-f0-9]{8})$' example: vact_app_0123456789abcdef01234567 CallId: name: callId in: path required: true schema: { type: string, pattern: '^[A-Za-z0-9_-]{1,80}$' } example: call_0123456789abcdef securitySchemes: appSecret: type: http scheme: bearer description: > Your App Secret (`vact_live_…`) as a Bearer token. **Server-side only** — never ship it in a browser, a mobile binary, or a URL. sessionToken: type: http scheme: bearer description: The session token (`vact_st_…`) returned by `/v1/session/exchange`. schemas: Error: type: object properties: error: type: object properties: code: { type: string } message: { type: string } example: error: { code: invalid_request, message: a required field is missing } responses: BadRequest: description: The request was malformed. content: application/json: schema: { $ref: '#/components/schemas/Error' } Unauthorized: description: Missing or invalid credential. content: application/json: schema: { $ref: '#/components/schemas/Error' } Forbidden: description: Authenticated but not permitted. content: application/json: schema: { $ref: '#/components/schemas/Error' } NotFound: description: Resource not found. content: application/json: schema: { $ref: '#/components/schemas/Error' } RateLimited: description: Too many requests. content: application/json: schema: { $ref: '#/components/schemas/Error' } # Backend endpoints use appSecret; most Client endpoints use sessionToken. # Per-operation security overrides this default where needed. security: - sessionToken: []