openapi: 3.2.0 info: title: ProofDraw Draws API version: 1.0.0 description: 'Provably fair winner/loser selection. Every draw is sealed with SHA-256, decided by the drand public randomness beacon, and independently verifiable by anyone — in the browser, from public sources alone. **Flow**: create a draw → add entries → seal (the entry list + a *future* drand round are hashed together and committed publicly) → when the round arrives, `winner_row = drand_value mod N`. Or do it all in one call with `POST /v1/draws/instant`. **Envelope**: every response is `{ "success": bool, "data": …, "message": string }`. Errors add a machine-readable `code`. **Rate limits**: per API key. `X-RateLimit-Limit` and `X-RateLimit-Remaining` headers are returned on every response. ' termsOfService: https://proofdraw.com/terms contact: name: ProofDraw email: hello@proofdraw.com url: https://proofdraw.com/contact servers: - url: https://proofdraw.com/api description: Production security: - bearerAuth: [] tags: - name: Draws paths: /v1/draws: get: tags: - Draws summary: List your draws description: Returns up to 100 most recent draws, newest first. responses: '200': description: Draws. content: application/json: schema: allOf: - $ref: '#/components/schemas/Envelope' - type: object properties: data: type: object properties: draws: type: array items: $ref: '#/components/schemas/Draw' '401': $ref: '#/components/responses/Unauthenticated' post: tags: - Draws summary: Create a draw description: 'Creates a draw in state `open`. If `callback_url` is set, the response includes `callback_secret` (HMAC key for webhook signatures) — this is the **only** time it is returned. ' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/DrawCreate' responses: '201': description: Draw created. content: application/json: schema: $ref: '#/components/schemas/DrawResponse' '401': $ref: '#/components/responses/Unauthenticated' '403': $ref: '#/components/responses/TierLimit' '422': $ref: '#/components/responses/ValidationFailed' /v1/draws/instant: post: tags: - Draws summary: Create, add entries, and seal in one call description: 'The recommended endpoint when you already have the full entrant list. With `wait: true`, blocks until the drand round arrives (~32–35 s with quicknet defaults) and returns the **resolved** draw with the winner. If create + entries succeed but the seal step fails, the open draw and tickets are returned with HTTP 500 / `code: seal_failed` — retry via `POST /v1/draws/{id}/seal`. ' requestBody: required: true content: application/json: schema: allOf: - $ref: '#/components/schemas/DrawCreate' - type: object required: - entries properties: entries: type: array minItems: 1 maxItems: 5000 items: $ref: '#/components/schemas/EntryInput' round_offset_seconds: type: integer minimum: 15 maximum: 604800 default: 30 wait: type: boolean default: false responses: '201': description: Draw sealed (and resolved, when `wait` was true and the round arrived in time). content: application/json: schema: $ref: '#/components/schemas/SealedDrawResponse' '401': $ref: '#/components/responses/Unauthenticated' '403': $ref: '#/components/responses/TierLimit' '409': $ref: '#/components/responses/StateConflict' '422': $ref: '#/components/responses/ValidationFailed' '500': description: 'Draw + entries persisted but the seal step failed (`code: seal_failed`). Retry the seal.' content: application/json: schema: $ref: '#/components/schemas/Error' /v1/draws/{id}: parameters: - $ref: '#/components/parameters/DrawId' get: tags: - Draws summary: Fetch a draw responses: '200': description: The draw in its current state. content: application/json: schema: $ref: '#/components/schemas/DrawResponse' '401': $ref: '#/components/responses/Unauthenticated' '404': $ref: '#/components/responses/NotFound' delete: tags: - Draws summary: Cancel an open draw description: Only draws in state `open` can be cancelled. Sealed draws are public commitments and cannot be erased. responses: '200': description: Draw cancelled. content: application/json: schema: $ref: '#/components/schemas/DrawResponse' '401': $ref: '#/components/responses/Unauthenticated' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/StateConflict' /v1/draws/{id}/entries: parameters: - $ref: '#/components/parameters/DrawId' post: tags: - Draws summary: Add entries in bulk description: 'Up to 5,000 entries per request. `ticket_id` is optional — omit it and the server generates a Crockford-Base32 ticket. Supplied tickets must match `[A-Za-z0-9_\-\.]+` and be unique within the draw. ' requestBody: required: true content: application/json: schema: type: object required: - entries properties: entries: type: array minItems: 1 maxItems: 5000 items: $ref: '#/components/schemas/EntryInput' responses: '201': description: Entries added. content: application/json: schema: allOf: - $ref: '#/components/schemas/Envelope' - type: object properties: data: type: object properties: added: type: integer entry_count: type: integer tickets: type: array items: type: string '401': $ref: '#/components/responses/Unauthenticated' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/StateConflict' '422': $ref: '#/components/responses/ValidationFailed' /v1/draws/{id}/seal: parameters: - $ref: '#/components/parameters/DrawId' post: tags: - Draws summary: Seal the draw description: 'Freezes the entry list: picks a future drand round, renders the canonical v2 list file (the round is in the header, so one SHA-256 binds list **and** round), pushes it to the public `proofdraw/draw-lists` git mirror, and submits the hash to an OpenTimestamps calendar. The commit must land at least 10 s before the round publishes or the seal aborts. With `wait: true`, holds the connection until the round arrives and returns the resolved draw (bounded by the server''s wait cap, default 60 s). ' requestBody: content: application/json: schema: type: object properties: round_offset_seconds: type: integer minimum: 15 maximum: 604800 default: 30 wait: type: boolean default: false responses: '200': description: Draw sealed (resolved when `wait` was true and the round arrived in time). content: application/json: schema: $ref: '#/components/schemas/SealedDrawResponse' '401': $ref: '#/components/responses/Unauthenticated' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/StateConflict' /v1/draws/{id}/resolve: parameters: - $ref: '#/components/parameters/DrawId' post: tags: - Draws summary: Resolve a sealed draw now description: 'Fetches the committed drand round and computes `winner_row = drand_value mod N`. Draws also auto-resolve within ~60 s of the round time; use this endpoint when you need the result the moment it is available. Idempotent on already-resolved draws. ' responses: '200': description: Resolved draw. content: application/json: schema: $ref: '#/components/schemas/DrawResponse' '401': $ref: '#/components/responses/Unauthenticated' '404': $ref: '#/components/responses/NotFound' '409': description: Not sealed (`state_conflict`), or the drand round has not been published yet (`not_yet_available`). content: application/json: schema: $ref: '#/components/schemas/Error' '503': description: drand API unreachable (`drand_unavailable`) — transient, retry. content: application/json: schema: $ref: '#/components/schemas/Error' components: schemas: SealedDrawResponse: allOf: - $ref: '#/components/schemas/Envelope' - type: object properties: data: type: object properties: draw: allOf: - $ref: '#/components/schemas/Draw' - type: object properties: commitment_text: type: string description: Publish this to a public timestamped channel before the round time.: null tweet_intent_url: type: string description: One-click X post of the commitment. tickets: type: array items: type: string description: 'Instant endpoint only: all ticket ids in submission order.' EntryInput: type: object properties: ticket_id: type: string maxLength: 64 pattern: ^[A-Za-z0-9_\-\.]+$ description: Optional — omit to auto-generate. Your own customer ids, order numbers, or email hashes all work. metadata: type: - object - 'null' description: Opaque to ProofDraw; stored as-is. Draw: type: object properties: id: type: string example: K7M2 name: type: string description: type: - string - 'null' state: type: string enum: - open - sealed - resolved - cancelled direction: type: string enum: - winner - loser winner_count: type: integer entry_count: type: integer drand_chain: type: string enum: - quicknet - classic drand_round: type: - integer - 'null' format: int64 drand_round_time: type: - string - 'null' format: date-time list_hash: type: - string - 'null' description: SHA-256 over the sealed list bytes. list_url: type: - string - 'null' verify_url: type: - string - 'null' description: Public per-draw verification page. winner_row: type: - integer - 'null' description: 0-based index into the sealed list. winner_ticket: type: - string - 'null' callback_url: type: - string - 'null' callback_secret: type: - string - 'null' description: Returned only on the creating response. HMAC key for webhook signatures. public_commit_url: type: - string - 'null' sealed_at: type: - string - 'null' format: date-time resolved_at: type: - string - 'null' format: date-time created_at: type: string format: date-time ots_proof_url: type: - string - 'null' ots_calendar_url: type: - string - 'null' ots_attested_at: type: - string - 'null' format: date-time Error: type: object properties: success: type: boolean example: false data: {} message: type: string code: type: string enum: - validation_failed - unauthenticated - tier_limit_exceeded - not_found - state_conflict - not_yet_available - entry_limit_exceeded - rate_limited - seal_failed - internal_error - drand_unavailable DrawCreate: type: object required: - name properties: name: type: string maxLength: 120 description: type: - string - 'null' maxLength: 2000 direction: type: string enum: - winner - loser default: winner winner_count: type: integer minimum: 1 maximum: 1 default: 1 description: Multi-winner draws are enterprise-only in v1. drand_chain: type: string enum: - quicknet - classic default: quicknet callback_url: type: - string - 'null' maxLength: 500 description: Webhook target for draw.sealed / draw.resolved / draw.cancelled events. metadata: type: - object - 'null' description: Opaque max 4 KB.: null Envelope: type: object properties: success: type: boolean example: true message: type: string example: '' DrawResponse: allOf: - $ref: '#/components/schemas/Envelope' - type: object properties: data: type: object properties: draw: $ref: '#/components/schemas/Draw' responses: StateConflict: description: Operation not valid in the draw's current state (`state_conflict`). content: application/json: schema: $ref: '#/components/schemas/Error' NotFound: description: Draw not found or not yours (`not_found`). content: application/json: schema: $ref: '#/components/schemas/Error' ValidationFailed: description: Invalid payload (`validation_failed` / `entry_limit_exceeded`). content: application/json: schema: $ref: '#/components/schemas/Error' TierLimit: description: Plan draw cap reached (`tier_limit_exceeded`). content: application/json: schema: $ref: '#/components/schemas/Error' Unauthenticated: description: Missing or invalid API key (`unauthenticated`). content: application/json: schema: $ref: '#/components/schemas/Error' parameters: DrawId: name: id in: path required: true description: Draw public id — 4 uppercase Crockford-Base32 characters. schema: type: string pattern: ^[0-9A-Z]{4}$ example: K7M2 securitySchemes: bearerAuth: type: http scheme: bearer description: 'API key: `Authorization: Bearer pd_live_…` (or `pd_test_…` for sandbox keys).' externalDocs: description: Human-readable API documentation url: https://proofdraw.com/api