ProofDraw API

Provably-fair random selection REST API (v1) covering draw lifecycle, account, health, and public verification artifacts. HMAC-signed webhooks. Bearer API-key auth.

OpenAPI Specification

proofdraw-api-openapi.yml Raw ↑
openapi: 3.0.3
info:
  title: ProofDraw 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
externalDocs:
  description: Human-readable API documentation
  url: https://proofdraw.com/api
servers:
- url: https://proofdraw.com/api
  description: Production
tags:
- name: System
- name: Auth
- name: Account
- name: Draws
- name: Verification
  description: Public, unauthenticated artifacts used to verify a draw.
security:
- bearerAuth: []
paths:
  /health:
    get:
      tags:
      - System
      summary: Health check
      security: []
      responses:
        '200':
          description: Service is up.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: ok
                  time:
                    type: string
                    format: date-time
  /v1/auth/login:
    post:
      tags:
      - Auth
      summary: Exchange email + password for a fresh API key
      description: |
        Each call issues a **new** key; existing keys keep working. The raw key
        is returned only in this response. Sandbox-tier users receive
        `pd_test_*` keys; all other tiers receive `pd_live_*`.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - email
              - password
              properties:
                email:
                  type: string
                  format: email
                password:
                  type: string
                  format: password
      responses:
        '200':
          description: Key issued.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthKeyResponse'
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '422':
          $ref: '#/components/responses/ValidationFailed'
  /v1/auth/register:
    post:
      tags:
      - Auth
      summary: Create a free-tier account
      description: |
        Creates a `free`-tier user (5 draws total lifetime, 100 entries per
        draw, 60 requests/min) and returns a `pd_live_*` API key.
        `terms_accepted_at` must be at or after the current terms effective
        date — send the moment of acceptance as an ISO-8601 timestamp.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - name
              - email
              - password
              - terms_accepted_at
              properties:
                name:
                  type: string
                  maxLength: 120
                email:
                  type: string
                  format: email
                password:
                  type: string
                  format: password
                  minLength: 8
                  maxLength: 255
                terms_accepted_at:
                  type: string
                  format: date-time
      responses:
        '201':
          description: Account created, key issued.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthKeyResponse'
        '422':
          $ref: '#/components/responses/ValidationFailed'
  /v1/me:
    get:
      tags:
      - Account
      summary: Current account, usage, and limits
      responses:
        '200':
          description: Account info.
          content:
            application/json:
              schema:
                allOf:
                - $ref: '#/components/schemas/Envelope'
                - type: object
                  properties:
                    data:
                      type: object
                      properties:
                        user:
                          $ref: '#/components/schemas/User'
                        profile:
                          allOf:
                          - $ref: '#/components/schemas/Profile'
                          nullable: true
                        usage:
                          $ref: '#/components/schemas/Usage'
                        limits:
                          $ref: '#/components/schemas/Limits'
        '401':
          $ref: '#/components/responses/Unauthenticated'
    patch:
      tags:
      - Account
      summary: Update name, email, or password
      description: Changing the password requires `current_password`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  maxLength: 120
                email:
                  type: string
                  format: email
                  maxLength: 160
                password:
                  type: string
                  format: password
                  minLength: 8
                  maxLength: 200
                current_password:
                  type: string
                  format: password
      responses:
        '200':
          description: Account updated.
          content:
            application/json:
              schema:
                allOf:
                - $ref: '#/components/schemas/Envelope'
                - type: object
                  properties:
                    data:
                      type: object
                      properties:
                        user:
                          $ref: '#/components/schemas/User'
                        profile:
                          allOf:
                          - $ref: '#/components/schemas/Profile'
                          nullable: true
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '422':
          $ref: '#/components/responses/ValidationFailed'
  /v1/me/profile:
    put:
      tags:
      - Account
      summary: Create or replace the account's business profile
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Profile'
      responses:
        '200':
          description: Profile saved.
          content:
            application/json:
              schema:
                allOf:
                - $ref: '#/components/schemas/Envelope'
                - type: object
                  properties:
                    data:
                      type: object
                      properties:
                        profile:
                          $ref: '#/components/schemas/Profile'
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '422':
          $ref: '#/components/responses/ValidationFailed'
  /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'
  /list/{hash}:
    servers:
    - url: https://proofdraw.com
    get:
      tags:
      - Verification
      summary: Download a sealed entry list
      description: |
        The exact bytes the published SHA-256 commits to (plain text, UTF-8,
        LF; `# `-prefixed header lines carry draw id, chain, round, round time
        and entry count, then one ticket per line). Re-hash the bytes to
        verify. Also mirrored at `github.com/proofdraw/draw-lists`.
      security: []
      parameters:
      - name: hash
        in: path
        required: true
        schema:
          type: string
          pattern: ^[0-9a-f]{64}$
      responses:
        '200':
          description: Sealed list bytes.
          content:
            text/plain:
              schema:
                type: string
        '404':
          description: Unknown hash.
  /list/{hash}/ots:
    servers:
    - url: https://proofdraw.com
    get:
      tags:
      - Verification
      summary: Download the OpenTimestamps proof for a sealed list
      description: Verify with `ots verify` against the list file. Calendar attestations upgrade to Bitcoin block anchors
        within ~24 h of sealing.
      security: []
      parameters:
      - name: hash
        in: path
        required: true
        schema:
          type: string
          pattern: ^[0-9a-f]{64}$
      responses:
        '200':
          description: Binary `.ots` proof.
          content:
            application/octet-stream:
              schema:
                type: string
                format: binary
        '404':
          description: No OTS proof for this hash.
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: 'API key: `Authorization: Bearer pd_live_…` (or `pd_test_…` for sandbox keys).'
  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
  responses:
    Unauthenticated:
      description: Missing or invalid API key (`unauthenticated`).
      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'
    NotFound:
      description: Draw not found or not yours (`not_found`).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    StateConflict:
      description: Operation not valid in the draw's current state (`state_conflict`).
      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'
  schemas:
    Envelope:
      type: object
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          example: ''
    Error:
      type: object
      properties:
        success:
          type: boolean
          example: false
        data:
          nullable: true
        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
    User:
      type: object
      properties:
        id:
          type: integer
        email:
          type: string
          format: email
        name:
          type: string
        tier:
          type: string
          enum:
          - free
          - starter
          - pro
          - enterprise
          - sandbox
    Profile:
      type: object
      properties:
        company_name:
          type: string
          nullable: true
          maxLength: 160
        website:
          type: string
          nullable: true
          maxLength: 255
        description:
          type: string
          nullable: true
          maxLength: 2000
        contact_name:
          type: string
          nullable: true
          maxLength: 120
        contact_email:
          type: string
          nullable: true
          maxLength: 160
        contact_phone:
          type: string
          nullable: true
          maxLength: 40
        address_line1:
          type: string
          nullable: true
          maxLength: 200
        address_line2:
          type: string
          nullable: true
          maxLength: 200
        city:
          type: string
          nullable: true
          maxLength: 120
        region:
          type: string
          nullable: true
          maxLength: 120
        postal_code:
          type: string
          nullable: true
          maxLength: 20
        country:
          type: string
          nullable: true
          minLength: 2
          maxLength: 2
          description: ISO 3166-1 alpha-2
    Usage:
      type: object
      properties:
        period:
          type: string
          enum:
          - lifetime
          - month
          description: Whether the draw limit resets monthly or is a lifetime cap (free tier).
        period_start:
          type: string
          format: date-time
        draws_this_period:
          type: integer
        draws_limit:
          type: integer
          nullable: true
          description: null = unlimited.
    Limits:
      type: object
      properties:
        entries_per_draw:
          type: integer
          nullable: true
        rate_per_minute:
          type: integer
          nullable: true
    AuthKeyResponse:
      allOf:
      - $ref: '#/components/schemas/Envelope'
      - type: object
        properties:
          data:
            type: object
            properties:
              api_key:
                type: string
                example: pd_live_8K3Q9R7M2X5V1N4B6C0D2E7F3G8H5J
              prefix:
                type: string
                example: pd_live_8K3Q
              user:
                $ref: '#/components/schemas/User'
    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
          nullable: true
          description: Opaque to ProofDraw; stored as-is.
    DrawCreate:
      type: object
      required:
      - name
      properties:
        name:
          type: string
          maxLength: 120
        description:
          type: string
          nullable: true
          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
          nullable: true
          maxLength: 500
          description: Webhook target for draw.sealed / draw.resolved / draw.cancelled events.
        metadata:
          type: object
          nullable: true
          description: Opaque
          max 4 KB.: null
    Draw:
      type: object
      properties:
        id:
          type: string
          example: K7M2
        name:
          type: string
        description:
          type: string
          nullable: true
        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
          format: int64
          nullable: true
        drand_round_time:
          type: string
          format: date-time
          nullable: true
        list_hash:
          type: string
          nullable: true
          description: SHA-256 over the sealed list bytes.
        list_url:
          type: string
          nullable: true
        verify_url:
          type: string
          nullable: true
          description: Public per-draw verification page.
        winner_row:
          type: integer
          nullable: true
          description: 0-based index into the sealed list.
        winner_ticket:
          type: string
          nullable: true
        callback_url:
          type: string
          nullable: true
        callback_secret:
          type: string
          nullable: true
          description: Returned only on the creating response. HMAC key for webhook signatures.
        public_commit_url:
          type: string
          nullable: true
        sealed_at:
          type: string
          format: date-time
          nullable: true
        resolved_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
        ots_proof_url:
          type: string
          nullable: true
        ots_calendar_url:
          type: string
          nullable: true
        ots_attested_at:
          type: string
          format: date-time
          nullable: true
    DrawResponse:
      allOf:
      - $ref: '#/components/schemas/Envelope'
      - type: object
        properties:
          data:
            type: object
            properties:
              draw:
                $ref: '#/components/schemas/Draw'
    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.'