OpenAPI
Source: docs/spec/openapi.yaml in the upstream repository.
openapi: 3.1.0info: title: Punch HTTP API summary: SRT session brokerage signalling service. description: | Punch lets two SRT endpoints discover each other and connect without manual IP exchange or port forwarding. The HTTP surface manages session lifecycle; real-time peer signalling lives on a separate WebSocket endpoint (see `/api/ws/{session}` and the companion JSON Schema at `punch-messages.schema.json`). version: 1.0.0 license: name: MIT identifier: MIT contact: name: Punch url: https://github.com/FiLORUX/punch
servers: - url: https://punch.thåst.se description: Production - url: http://localhost:8787 description: Local wrangler dev
tags: - name: health description: Liveness probes for monitoring. - name: session description: Session lifecycle — create, query, delete. - name: connect description: Connection-string generation. - name: ui description: Server-rendered web UI assets.
paths: /api/health: get: tags: [health] summary: Service health probe description: Returns service version. Use this for uptime monitoring. operationId: getHealth responses: '200': description: Service is up. headers: Punch-Version: schema: { type: string } description: Service version. content: application/json: schema: $ref: '#/components/schemas/HealthResponse' head: tags: [health] summary: Service health probe (HEAD) description: HEAD variant for monitors that probe with HEAD by default. operationId: headHealth responses: '200': description: Service is up; headers only, no body.
/api/session: post: tags: [session] summary: Create a session description: | Creates a new session and returns its admin token. The token is returned exactly once and grants administrative control (close session, observe `peer_match` events). Distribute peer URLs without the token when only join-as-sender / join-as-receiver flows are needed. operationId: createSession security: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateSessionRequest' examples: minimal: summary: Minimal — single default stream value: { name: nab-floor-cam1 } multiCam: summary: Multi-camera value: name: nab-iso-shoot streams: [cam-wide, cam-close, pgm-return] latency: 200 ttl: 3600 responses: '201': description: Session created. content: application/json: schema: $ref: '#/components/schemas/CreateSessionResponse' '400': $ref: '#/components/responses/BadRequest' '403': description: Turnstile verification failed. content: application/json: schema: { $ref: '#/components/schemas/Error' } '409': description: A session with this name already exists. content: application/json: schema: { $ref: '#/components/schemas/Error' } '429': $ref: '#/components/responses/RateLimited'
/api/session/{name}: parameters: - $ref: '#/components/parameters/SessionName' get: tags: [session] summary: Get session state operationId: getSession security: - bearerAuth: [] responses: '200': description: Session state. content: application/json: schema: $ref: '#/components/schemas/SessionState' '401': $ref: '#/components/responses/Unauthorised' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' delete: tags: [session] summary: Close session (admin) description: Permanently terminates the session. Admin token only. operationId: deleteSession security: - bearerAuth: [] responses: '204': description: Session closed. '401': $ref: '#/components/responses/Unauthorised' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound'
/api/session/{name}/connect: parameters: - $ref: '#/components/parameters/SessionName' - in: query name: stream required: false schema: $ref: '#/components/schemas/StreamName' description: Stream identifier; defaults to the session's first stream. - in: query name: role required: false schema: type: string enum: [sender, receiver] default: sender description: | Local SRT role. Affects format-specific output (e.g. `ffmpeg` flips between `-i` and stdin pipelines). Has no effect on the generated SRT URI itself — rendezvous mode is symmetric. get: tags: [connect] summary: Get connection strings for both peers description: | Returns ready-to-paste connection strings for both matched peers. Available only after both peers in the stream have registered (session state is `READY` or later).
The response carries two parallel format maps. `for_peer_a` is the set of strings peer A should run (targeting peer B); `for_peer_b` is the mirror. Both maps cover every supported encoder format. operationId: getConnectionStrings security: - bearerAuth: [] responses: '200': description: Connection strings for both peers, by encoder format. content: application/json: schema: $ref: '#/components/schemas/ConnectionStringsResponse' '400': description: Peers not yet registered for this stream. content: application/json: schema: { $ref: '#/components/schemas/Error' } '401': $ref: '#/components/responses/Unauthorised' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound'
/api/ws/{name}: parameters: - $ref: '#/components/parameters/SessionName' get: tags: [session] summary: Open the signalling WebSocket description: | Upgrades the connection to a WebSocket. After the upgrade the connection speaks the message envelope defined in `punch-messages.schema.json`. Authentication is performed via the `t` query string token because most browser WebSocket clients cannot set custom headers on the upgrade request. operationId: openWebSocket parameters: - in: query name: t required: true schema: { type: string } description: Session token (admin or peer). responses: '101': description: WebSocket upgrade succeeded. '401': $ref: '#/components/responses/Unauthorised' '404': $ref: '#/components/responses/NotFound' '426': description: Upgrade required — the request did not include the WebSocket upgrade headers.
/s/{name}: parameters: - $ref: '#/components/parameters/SessionName' get: tags: [ui] summary: Session dashboard (HTML) description: | Server-rendered dashboard. The optional `t` query parameter unlocks admin observer mode; without it the page renders peer-join controls. operationId: renderDashboard parameters: - in: query name: t required: false schema: { type: string } responses: '200': description: HTML page. content: text/html: {}
/s/{name}/qr: parameters: - $ref: '#/components/parameters/SessionName' get: tags: [ui] summary: Session QR code (SVG) operationId: renderQrCode responses: '200': description: QR code as SVG. content: image/svg+xml: {}
/: get: tags: [ui] summary: Landing page (HTML) description: Create-session form and feature overview. operationId: renderLandingPage responses: '200': description: HTML page. content: text/html: {}
components: parameters: SessionName: in: path name: name required: true schema: type: string pattern: '^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$' maxLength: 64 description: 1–64 characters; alphanumeric plus '.', '_', '-'.
securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: punch-token description: | Token returned by `POST /api/session`. Format: `p_<base64url-payload>.<base64url-hmac>` (HMAC-SHA256).
responses: BadRequest: description: Invalid request body or parameters. content: application/json: schema: { $ref: '#/components/schemas/Error' } Unauthorised: description: Missing or malformed authentication. content: application/json: schema: { $ref: '#/components/schemas/Error' } Forbidden: description: Token is not valid for this resource. content: application/json: schema: { $ref: '#/components/schemas/Error' } NotFound: description: No session with the given name exists. content: application/json: schema: { $ref: '#/components/schemas/Error' } RateLimited: description: Per-IP rate limit exceeded. headers: Retry-After: schema: { type: integer, description: Seconds to wait before retrying. } content: application/json: schema: { $ref: '#/components/schemas/Error' }
schemas: HealthResponse: type: object required: [status, version] properties: status: { type: string, const: ok } version: { type: string, examples: ['1.0.0'] }
CreateSessionRequest: type: object required: [name] properties: name: type: string pattern: '^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$' maxLength: 64 streams: type: array items: type: string pattern: '^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$' maxItems: 16 description: Optional stream identifiers; default is a single 'default' stream. latency: type: integer minimum: 20 maximum: 8000 default: 200 description: Suggested SRT latency in milliseconds. ttl: type: integer minimum: 60 maximum: 86400 default: 1800 description: Session lifetime in seconds. turnstileToken: type: string description: | Cloudflare Turnstile widget response token. Required when the deployment has `TURNSTILE_SECRET` configured.
CreateSessionResponse: type: object required: [session, token, url, qr, streams, latency, ttl, state, created] properties: session: { type: string } token: { type: string, description: Admin token. } url: { type: string, format: uri } qr: { type: string, format: uri } streams: { type: array, items: { type: string } } latency: { type: integer } ttl: { type: integer } state: { type: string, const: WAITING } created: { type: string, format: date-time }
SessionState: type: object required: [session, state, streams, expiresAt] properties: session: { type: string } state: type: string enum: [WAITING, READY, CONNECTED, RELAYING, CLOSED] streams: type: object additionalProperties: type: object properties: state: { type: string } peers: { type: integer } passphrase: { type: string, description: Present only when authenticated. } localPort: { type: integer } expiresAt: { type: integer, description: Unix epoch milliseconds. } latency: { type: integer }
ConnectionFormatMap: type: object required: [ffmpeg, obs, vmix, gstreamer, srt-live-transmit] properties: ffmpeg: { type: string, description: Full FFmpeg invocation, ready to paste. } obs: type: string description: | Bare SRT URI for OBS Studio's "Server" field. Includes the `localip`/`localport` parameters required by OBS rendezvous. vmix: type: string description: | Field-by-field text (vMix has no URI paste). Renders as a multi-line block listing Hostname, Port, Local Port, Latency, Passphrase, Mode and Key Length for manual entry. gstreamer: type: string description: | `srtsrc`/`srtsink` element snippet, direction-aware via the `role` query parameter. 'srt-live-transmit': type: string description: Full `srt-live-transmit` invocation bridging UDP loopback to SRT.
ConnectionStringsResponse: type: object required: [stream, role, for_peer_a, for_peer_b] properties: stream: { $ref: '#/components/schemas/StreamName' } role: type: string enum: [sender, receiver] description: Echo of the requested local role. for_peer_a: $ref: '#/components/schemas/ConnectionFormatMap' description: Strings for peer A to run, targeting peer B's endpoint. for_peer_b: $ref: '#/components/schemas/ConnectionFormatMap' description: Strings for peer B to run, targeting peer A's endpoint. description: | Symmetric pair of connection-string sets: each map is keyed by encoder format. Producers typically dispatch `for_peer_a` to one operator and `for_peer_b` to the other.
StreamName: type: string pattern: '^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$' maxLength: 64 description: Stream identifier within a session. Same character class as session names.
Error: type: object required: [error, message] properties: error: type: string enum: - SESSION_FULL - SESSION_EXPIRED - STREAM_TAKEN - AUTH_FAILED - INVALID_MESSAGE - SERVER_ERROR - SESSION_EXISTS - RATE_LIMITED - TURNSTILE_FAILED message: { type: string }