openapi: 3.1.0
info:
  title: PLLAY Outcomes Oracle — Settlement
  version: 1.0.0
  description: |
    Contract for the settlement machine. States: open | locked | held | settled | reversed | voided.
    Transitions: open→locked|voided · locked→held|settled|voided · held→settled|voided · settled→reversed.
    reversed and voided are terminal. held is a success state and pays nobody.
servers:
  - url: https://api.pllay.io
security:
  - PartnerKey: []
components:
  securitySchemes:
    PartnerKey: { type: apiKey, in: header, name: X-Partner-Key }
  parameters:
    EventId: { name: id, in: path, required: true, schema: { type: string } }
    IdempotencyKey: { name: Idempotency-Key, in: header, required: true, schema: { type: string } }
  schemas:
    State: { type: string, enum: [open, locked, held, settled, reversed, voided] }
    Type: { type: string, enum: [promo, contest, duel] }
    Path: { type: string, enum: [vod, live] }
    EventCreate:
      type: object
      required: [type, path, spec, threshold]
      properties:
        type: { $ref: '#/components/schemas/Type' }
        path: { $ref: '#/components/schemas/Path' }
        spec: { type: object, description: Title, market and rule the outcome is judged against. }
        threshold: { type: number, minimum: 0, maximum: 1 }
        stake_ref: { type: [string, 'null'], description: Forbidden when type=promo (422 promo_stake). }
        notional_minor: { type: integer, minimum: 0 }
    Event:
      type: object
      additionalProperties: false
      required: [event_id, type, path, state, spec, outcome, confidence, threshold, model_version, evidence_uri, settlement_id, reviewer_id, stake_ref, notional_minor, reason]
      properties:
        event_id: { type: string }
        spec: { type: object }
        path: { $ref: '#/components/schemas/Path' }
        type: { $ref: '#/components/schemas/Type' }
        state: { $ref: '#/components/schemas/State' }
        outcome: { type: [string, 'null'] }
        model_version: { type: [string, 'null'] }
        confidence: { type: [number, 'null'], minimum: 0, maximum: 1 }
        threshold: { type: number, minimum: 0, maximum: 1 }
        evidence_uri: { type: [string, 'null'], format: uri }
        settlement_id: { type: [string, 'null'] }
        reviewer_id: { type: [string, 'null'] }
        stake_ref: { type: [string, 'null'] }
        notional_minor: { type: integer, minimum: 0 }
        reason: { type: [string, 'null'] }
    Settle:
      type: object
      required: [outcome, confidence, model_version, evidence_uri]
      properties:
        outcome: { type: string }
        confidence: { type: number, minimum: 0, maximum: 1 }
        model_version: { type: string }
        evidence_uri: { type: string, format: uri }
        reviewer_id: { type: [string, 'null'], description: Required when path=live (422 live_unreviewed). }
    Reason:
      type: object
      properties:
        reason: { type: string }
        reviewer_id: { type: [string, 'null'] }
    Error:
      type: object
      required: [error, message]
      properties:
        error: { type: string, enum: [live_unreviewed, below_threshold, promo_stake, illegal_transition] }
        message: { type: string }
  responses:
    Event: { description: Event after transition, content: { application/json: { schema: { $ref: '#/components/schemas/Event' } } } }
    Unprocessable: { description: Rule violation (below_threshold | live_unreviewed | promo_stake), content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
    Conflict: { description: illegal_transition — move not in the state map, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
paths:
  /v1/events:
    post:
      operationId: createEvent
      summary: Create an event → open
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/EventCreate' } } } }
      responses: { '201': { $ref: '#/components/responses/Event' }, '422': { $ref: '#/components/responses/Unprocessable' } }
  /v1/events/{id}/lock:
    post:
      operationId: lockEvent
      summary: open → locked
      parameters: [{ $ref: '#/components/parameters/EventId' }]
      responses: { '200': { $ref: '#/components/responses/Event' }, '422': { $ref: '#/components/responses/Unprocessable' }, '409': { $ref: '#/components/responses/Conflict' } }
  /v1/events/{id}/hold:
    post:
      operationId: holdEvent
      summary: locked → held
      parameters: [{ $ref: '#/components/parameters/EventId' }]
      requestBody: { content: { application/json: { schema: { $ref: '#/components/schemas/Reason' } } } }
      responses: { '200': { $ref: '#/components/responses/Event' }, '422': { $ref: '#/components/responses/Unprocessable' }, '409': { $ref: '#/components/responses/Conflict' } }
  /v1/events/{id}/settle:
    post:
      operationId: settleEvent
      summary: locked | held → settled
      description: 422 below_threshold when confidence < threshold (send hold). 422 live_unreviewed when path=live and no reviewer_id.
      parameters: [{ $ref: '#/components/parameters/EventId' }]
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/Settle' } } } }
      responses: { '200': { $ref: '#/components/responses/Event' }, '422': { $ref: '#/components/responses/Unprocessable' }, '409': { $ref: '#/components/responses/Conflict' } }
  /v1/events/{id}/void:
    post:
      operationId: voidEvent
      summary: open | locked | held → voided
      parameters: [{ $ref: '#/components/parameters/EventId' }]
      requestBody: { content: { application/json: { schema: { $ref: '#/components/schemas/Reason' } } } }
      responses: { '200': { $ref: '#/components/responses/Event' }, '422': { $ref: '#/components/responses/Unprocessable' }, '409': { $ref: '#/components/responses/Conflict' } }
  /v1/events/{id}/reverse:
    post:
      operationId: reverseEvent
      summary: settled → reversed (writes a compensating ledger line)
      parameters: [{ $ref: '#/components/parameters/EventId' }]
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/Reason' } } } }
      responses: { '200': { $ref: '#/components/responses/Event' }, '422': { $ref: '#/components/responses/Unprocessable' }, '409': { $ref: '#/components/responses/Conflict' } }
  /v1/sandbox/fixture:
    post:
      operationId: sandboxFixture
      summary: Run a sandbox fixture; settles or holds
      requestBody: { content: { application/json: { schema: { type: object, properties: { hold: { type: boolean } } } } } }
      responses: { '200': { $ref: '#/components/responses/Event' } }
webhooks:
  event.settled: { post: { requestBody: { content: { application/json: { schema: { $ref: '#/components/schemas/Event' } } } }, responses: { '200': { description: ack } } } }
  event.held: { post: { requestBody: { content: { application/json: { schema: { $ref: '#/components/schemas/Event' } } } }, responses: { '200': { description: ack } } } }
  event.reversed: { post: { requestBody: { content: { application/json: { schema: { $ref: '#/components/schemas/Event' } } } }, responses: { '200': { description: ack } } } }
  event.voided: { post: { requestBody: { content: { application/json: { schema: { $ref: '#/components/schemas/Event' } } } }, responses: { '200': { description: ack } } } }
