openapi: 3.1.0
info:
  title: Redemption
  version: 1.0.0
  description: |
    Redeem a Motionworks credit code — the codes handed out at conferences, by partners such as Geopath, or directly by Motionworks. Two steps:

    1. **Validate** — anonymous. Checks a code and returns what it grants (credits, expiry, the audience it was issued for) without changing anything, so you can show the user what they are about to redeem.
    2. **Complete** — signed in. Adds the credits to the signed-in user's account exactly once and returns the new balance plus where to send the user next.

    Both operations are free. Unlike the data APIs, success bodies are returned as bare objects rather than inside a `{ data, meta }` wrapper; errors use the standard envelope (`code`, `message`, `status`, `request_id`, `product`, `docs_url`).
  contact:
    name: Motionworks AI
    url: https://mworks.com
    email: api@mworks.com
servers:
  - url: https://api2.mworks.com/v2
    description: Production
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        Signed-in session token (`Authorization: Bearer <jwt>`), as issued when the user signs in to the Motionworks console or app.
  schemas:
    ValidateRequest:
      type: object
      required:
        - code
      properties:
        code:
          type: string
          pattern: ^MW-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$
          description: The redemption code, in `MW-XXXX-XXXX-XXXX` form (upper-case letters and digits).
          example: MW-OAAA-2026-0001
      description: The code to check.
    ValidateSuccess:
      type: object
      required:
        - code_source
        - recipient_class
        - credits_allocated
        - expires_at
        - default_path
      properties:
        code_source:
          type: string
          enum:
            - geopath
            - conference
            - direct
          description: |
            Where the code came from: `geopath` (issued through Geopath), `conference` (an event code) or `direct` (issued by Motionworks).
        recipient_class:
          type: string
          enum:
            - operator
            - agency
            - researcher
            - brand
          description: 'The kind of organization the code was issued for: `operator`, `agency`, `researcher` or `brand`.'
        credits_allocated:
          type: integer
          minimum: 1
          example: 10000
          description: Credits the code adds when redeemed.
        expires_at:
          type: string
          format: date-time
          description: When the code stops being redeemable (ISO 8601).
        default_path:
          type: string
          enum:
            - app
            - api
          description: |
            The path Motionworks suggests offering first: `app` (the Insights Suite) or `api` (the Motionworks console).
      description: What a valid, unredeemed code grants.
    CompleteRequest:
      type: object
      required:
        - code
        - chosen_path
      properties:
        code:
          type: string
          pattern: ^MW-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$
          description: The redemption code, in `MW-XXXX-XXXX-XXXX` form (upper-case letters and digits).
        chosen_path:
          type: string
          enum:
            - app
            - api
          description: |
            Which surface the user chose: `app` (the Insights Suite) or `api` (the Motionworks console). Determines `redirect_url`.
      description: The code to redeem and the path the user chose.
    CompleteSuccess:
      type: object
      required:
        - success
        - credits_added
        - new_balance
        - new_expiry
        - redirect_url
        - telemetry_id
      properties:
        success:
          type: boolean
          enum:
            - true
          description: Always `true` on a successful redemption.
        credits_added:
          type: integer
          example: 10000
          description: Credits added to the account by this code.
        new_balance:
          type: integer
          example: 15000
          description: The account's credit balance after redemption.
        new_expiry:
          type: string
          format: date-time
          description: When the account's credits now expire (ISO 8601).
        redirect_url:
          type: string
          format: uri
          description: |
            Where to send the user next: the Insights Suite welcome page for `app`, or the Motionworks console for `api`.
        telemetry_id:
          type: string
          format: uuid
          description: Unique id for this redemption. Quote it when contacting support.
      description: The outcome of a successful redemption.
    Error:
      type: object
      required:
        - error
      properties:
        error:
          type: object
          required:
            - code
            - message
            - status
            - request_id
            - product
            - docs_url
          properties:
            code:
              type: string
              description: Stable machine-readable error code (snake_case).
            message:
              type: string
              description: Plain-English explanation, safe to show to the user.
            status:
              type: integer
              description: HTTP status code, repeated in the body.
            request_id:
              type: string
              format: uuid
              description: Unique id for this request. Quote it when contacting support.
            product:
              type: string
              enum:
                - router
              description: The Motionworks service that produced the error.
            docs_url:
              type: string
              format: uri
              description: Link to the documentation for this error code.
          description: 'The error: a stable `code`, a human-readable `message`, and the HTTP `status`.'
      description: The standard error envelope.
paths:
  /redemption/validate:
    post:
      operationId: validateRedemptionCode
      summary: Check a redemption code
      tags:
        - Redemption
      description: |
        Checks a code before the user redeems it and returns what it grants: where the code came from, who it was issued for, how many credits it adds, when it expires and which path Motionworks suggests. Nothing is changed. No authentication and no charge.

        Codes that are invalid, expired or unknown all return the same `404 redemption_unavailable` response, and each of those responses takes at least the same minimum time, so codes cannot be probed by timing. The one case you can tell apart before redeeming is a code that has already been used: `410 already_redeemed`.
      x-motionworks-status: customer
      x-credit-cost: 0
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ValidateRequest'
      responses:
        '200':
          description: The code is valid and has not been redeemed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidateSuccess'
        '404':
          description: The code is invalid, expired or unknown — deliberately indistinguishable (`redemption_unavailable`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '410':
          description: The code has already been redeemed (`already_redeemed`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /redemption/complete:
    post:
      operationId: completeRedemption
      summary: Redeem a code
      tags:
        - Redemption
      description: |
        Adds the code's credits to the signed-in user's account and returns the credits added, the new balance and expiry, and the URL to send the user to next — the Insights Suite for `chosen_path: app`, the Motionworks console for `chosen_path: api`. Requires a signed-in session (`Authorization: Bearer <jwt>`). No charge.

        Redemption happens exactly once. The required `Idempotency-Key` header (a UUID you generate) makes retries safe: repeating a call with the same key and the same body within 24 hours returns the original response (after that, a repeat reaches the already-used code and returns `410 already_redeemed`); the same key with a different body returns `409 idempotency_mismatch`; a missing header returns `400 missing_idempotency_key` and a non-UUID value `400 invalid_idempotency_key`. As with validation, an invalid, expired or unknown code returns the opaque `404 redemption_unavailable`, and a used code `410 already_redeemed`.
      x-motionworks-status: customer
      x-credit-cost: 0
      security:
        - bearerAuth: []
      parameters:
        - in: header
          name: Idempotency-Key
          required: true
          schema:
            type: string
            format: uuid
          description: |
            A UUID you generate for this redemption attempt. Reuse it when retrying so the code is never redeemed twice.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CompleteRequest'
      responses:
        '200':
          description: Redeemed. The credits are on the account.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CompleteSuccess'
        '400':
          description: |
            `missing_idempotency_key` (header absent), `invalid_idempotency_key` (header is not a UUID), or `invalid_chosen_path` (`chosen_path` is not `app` or `api`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: |
            Missing or invalid session token (`unauthenticated`) — the user must sign in or sign up before redeeming.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: The code is invalid, expired or unknown — deliberately indistinguishable (`redemption_unavailable`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: This `Idempotency-Key` was already used with a different request body (`idempotency_mismatch`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '410':
          description: The code has already been redeemed (`already_redeemed`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: |
            Redemption could not be completed because of a temporary fault on our side (`redemption_failed`). Retry with the same `Idempotency-Key`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
x-tagGroups:
  - name: Redemption
    tags:
      - Redemption
tags:
  - name: Redemption
    description: Check a credit code anonymously, then redeem it once for the signed-in user. Free.
x-customer-voice: true
