# Brizz Model C — Verification Provider contract v1 (PARTNER_CODES_SPEC.md §16.12).
# OPPOSITE DIRECTION to the Partner API: here BRIZZ IS THE CLIENT and the PARTNER
# IMPLEMENTS THIS ENDPOINT. Brizz publishes the spec (platform-defined callback,
# RFC 7662-style); the partner builds an endpoint that satisfies it.
# Additive changes only within v1; breaking changes mint v2.
openapi: 3.0.3
info:
  title: Brizz Model C — Verification Provider
  version: "1.0"
  description: |
    The HTTPS endpoint YOU host and Brizz calls, live, during checkout, to ask
    whether one of your codes is valid. Your answer decides whether the discount
    applies.

    **You are the server.** Brizz calls this when a customer enters one of your
    codes that Brizz does not already hold, with the customer waiting — so it
    must be online, fast and authenticated. Answers are cached briefly
    (approvals 2 min, denials 1 min, failures 30 s), and a code a customer has
    taken to payment is stored by Brizz and never sent to you again. Default
    timeout 1500 ms; Brizz can set it anywhere from 100 to 5000 ms, and the
    whole call, connecting included, must finish inside it. Slower is treated
    as unavailable: the code is refused for now ("This code can't be applied
    right now") and the customer can remove it and pay full price.

    **Authentication (Brizz to you).** Brizz presents a bearer credential as
    `Authorization: Bearer <secret>` OR `X-API-Key: <secret>` — those two header
    names only. Brizz can mint the secret and reveal it once, or present one you
    supply. Verify it in constant time.

    **Optional HMAC.** When signing is enabled, Brizz adds `X-Brizz-Timestamp`,
    `X-Brizz-Request-Id` and `X-Brizz-Signature: v1=<hex>` — HMAC-SHA256 over
    `{timestamp}.{request_id}.{raw_body}` (more than one `v1=` during a secret
    rotation). Verify against the RAW body; reject if `|now - timestamp| > 300 s`.

    **Codes Brizz will send.** Uppercased, trimmed, `[A-Z0-9-]`, 4-32 chars.
    Brizz routes a code to you by a prefix token `[A-Z0-9]{2,16}-` on the events
    its campaign covers, or by a prefixless mapping tied to specific events, in
    which case the code carries no prefix. A longer or non-conforming code, or
    one matching no route, is rejected by Brizz locally and never reaches you.

    **Response contract.** Exactly HTTP 200, `application/json`, at most 64 KiB,
    with a strict boolean `approved`. Anything else — another status, a non-JSON
    body, a timeout, a truthy string or number — is treated as unavailable.

    **A verification is not a redemption.** Approving reserves nothing on your
    side; the customer may still abandon checkout. Learn the real outcome from the
    `code.redeemed` webhook or the reconciliation report (Partner API), both of
    which carry the code and your `authorization_ref`.

    **No customer PII is sent.** You issued the code to a specific person, so the
    code is already your user reference.
servers:
  - url: https://your-endpoint.example
    description: The URL you host and register with Brizz (per campaign/route).
paths:
  /your/verification/path:
    post:
      summary: Verify one code (Brizz calls you)
      description: >
        Brizz asks whether `code` is valid for this campaign. Answer yes or no
        from your own records. The path is whatever URL you register with Brizz.
      operationId: verifyCode
      security:
        - bearerSecret: []
        - apiKey: []
      parameters:
        - { name: X-Brizz-Timestamp, in: header, required: false, schema: { type: string }, description: "Unix seconds; present when HMAC signing is enabled." }
        - { name: X-Brizz-Request-Id, in: header, required: false, schema: { type: string, format: uuid }, description: "Echoes body.request_id; present when signing is enabled." }
        - { name: X-Brizz-Signature, in: header, required: false, schema: { type: string, example: "v1=9a3b" }, description: "One or more v1= HMAC digests; present when signing is enabled." }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/VerificationRequest" }
      responses:
        "200":
          description: >
            Your verdict. MUST be exactly 200 + application/json + a strict
            boolean `approved`, at most 64 KiB. Any other status or body shape
            is treated as unavailable (no discount; checkout continues).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/VerificationResponse" }
components:
  securitySchemes:
    bearerSecret:
      type: http
      scheme: bearer
      description: "Authorization: Bearer <secret> — the credential Brizz presents."
    apiKey:
      type: apiKey
      in: header
      name: X-API-Key
      description: "Alternative to Bearer; one or the other, never both."
  schemas:
    VerificationRequest:
      type: object
      required: [code, campaign_uuid, request_id]
      properties:
        code: { type: string, minLength: 4, maxLength: 32, example: "PARTNER-AB12", description: "Uppercased, trimmed, [A-Z0-9-]." }
        campaign_uuid: { type: string, format: uuid }
        request_id: { type: string, format: uuid, description: "Correlation id; also in X-Brizz-Request-Id when signing is on. Log it." }
    VerificationResponse:
      type: object
      required: [approved]
      properties:
        approved:
          type: boolean
          description: Strict JSON boolean. A truthy string or number is rejected as malformed.
        authorization_ref:
          type: string
          maxLength: 128
          nullable: true
          description: >
            Optional opaque, non-PII reference for your own reconciliation, at
            most 128 BYTES. Echoed back later in the code.redeemed webhook and
            the report. An over-length or non-string value is DROPPED entirely
            (never truncated); the approval stands.
        reason:
          type: string
          nullable: true
          enum: [INVALID, EXPIRED, ALREADY_USED, NOT_ELIGIBLE, REVOKED]
          description: >
            Optional, closed vocabulary, cut to 256 bytes. Case does not matter,
            and spaces, hyphens and dots are treated alike, between words and at
            either end. A value outside the set is replaced with OTHER_<12 hex>,
            never stored raw. Never shown to the customer.
      example:
        approved: false
        reason: ALREADY_USED
