openapi: 3.1.0
info:
  title: AgentRefills API
  version: "1.0.0"
  summary: The gift-card purchasing API for AI agents.
  description: |
    AgentRefills lets an AI agent buy gift cards from 5,000+ brands through five
    operations, wrapped in per-agent spend caps, human-in-the-loop approval,
    fail-closed policy, encrypted single-read codes, and a full audit trail.
    Built on the Bitrefill network.

    ## Conventions
    - **Money is integer minor units.** `amount_minor: 2500` means $25.00. There are no floats.
    - **Auth.** Send `Authorization: Bearer <API_KEY>` on every call. Each key is scoped to one
      agent with its own daily/monthly caps and brand allowlist.
    - **Idempotency.** Supply `client_ref` on a purchase; re-using it returns the existing task
      and never charges twice.
    - **Async.** `POST /v1/purchases` returns immediately with a `task_id`. Poll
      `GET /v1/purchases/{task_id}` or subscribe to the `purchase.completed` webhook, then
      release the code once with `POST /v1/purchases/{task_id}/code`.
    - **Errors** use a closed taxonomy (see the `Error` schema). Every error carries an
      agent-actionable `hint`. `RETRYABLE` always includes `retry_after` seconds.

    ## The five operations map 1:1 to the MCP tools
    `list_brands` · `buy_giftcard` · `get_code` · `check_balance` · `request_approval`.
    The MCP server exposes the same semantics over Streamable HTTP at `https://agentrefills.com/mcp`.
  contact:
    name: AgentRefills
    url: https://agentrefills.com/docs
    email: hello@agentrefills.com
  license:
    name: Proprietary
    url: https://agentrefills.com/terms
  termsOfService: https://agentrefills.com/terms
externalDocs:
  description: Agent integration guide, per-SDK recipes, and the full llms.txt.
  url: https://agentrefills.com/docs
servers:
  - url: https://agentrefills.com
    description: Production base URL. All paths are versioned under /v1.
security:
  - bearerAuth: []
tags:
  - name: Catalog
    description: Discover purchasable brands.
  - name: Purchases
    description: Create a purchase, poll it, and release the code.
  - name: Account
    description: Budget and platform status for the calling agent.
  - name: Approvals
    description: Human-in-the-loop approval for gated purchases.
  - name: Webhooks
    description: Signed, deduplicated event delivery.
paths:
  /v1/oauth/token:
    post:
      operationId: oauth_token
      tags: [Auth]
      summary: Mint a bearer token (client_credentials)
      security: []
      description: |
        Exchange an operator-issued `client_id` + `client_secret` for a short-TTL bearer token
        whose subject is the agent_id. Accepts `application/x-www-form-urlencoded` (RFC 6749)
        or JSON. Send the token as `Authorization: Bearer <access_token>` on every call.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [grant_type, client_id, client_secret]
              properties:
                grant_type: {type: string, enum: [client_credentials]}
                client_id: {type: string}
                client_secret: {type: string}
                scope: {type: string, description: "Space-separated; defaults to all the client's scopes."}
            example: {grant_type: client_credentials, client_id: agent-acme-shopper, client_secret: "..."}
      responses:
        "200":
          description: Token issued.
          content:
            application/json:
              schema:
                type: object
                required: [access_token, token_type, expires_in]
                properties:
                  access_token: {type: string}
                  token_type: {type: string, enum: [Bearer]}
                  expires_in: {type: integer}
                  scope: {type: string}
        "400": {description: "unsupported_grant_type / invalid_scope"}
        "401": {description: "invalid_client (unknown client or bad secret)"}
  /v1/brands:
    get:
      operationId: list_brands
      x-mcp-tool: list_brands
      tags: [Catalog]
      summary: Search purchasable gift-card brands
      description: |
        Returns brands the calling agent may buy, filtered by query, country, and category.
        Backed by a 15-minute cache. Read-only and idempotent.
      parameters:
        - name: query
          in: query
          description: Free-text brand search, e.g. "amazon".
          schema: {type: string}
          example: amazon
        - name: country
          in: query
          description: ISO-3166-1 alpha-2 country code.
          schema: {type: string, default: US}
          example: US
        - name: category
          in: query
          description: Optional category filter, e.g. "gaming".
          schema: {type: string}
        - name: limit
          in: query
          schema: {type: integer, default: 20, maximum: 50, minimum: 1}
      responses:
        "200":
          description: Matching brands.
          content:
            application/json:
              schema:
                type: object
                required: [brands]
                properties:
                  brands:
                    type: array
                    items: {$ref: "#/components/schemas/Brand"}
              example:
                brands:
                  - brand_id: amazon_com-usa
                    name: Amazon
                    country: US
                    currency: USD
                    denominations_minor: [1000, 2500, 5000, 10000]
                    min_minor: 1000
                    max_minor: 200000
                    in_stock: true
        "401": {$ref: "#/components/responses/Error"}
        "429": {$ref: "#/components/responses/RateLimited"}
        default: {$ref: "#/components/responses/Error"}
  /v1/purchases:
    post:
      operationId: buy_giftcard
      x-mcp-tool: buy_giftcard
      tags: [Purchases]
      summary: Start a gift-card purchase (async)
      description: |
        Starts an asynchronous purchase and returns a `task_id` immediately. Under the agent's
        spend cap it settles automatically; at or above the approval threshold it returns a task
        in state `awaiting_approval` (a human approves in Slack). Supply `client_ref` for
        idempotency: re-using it returns the existing task and never charges twice.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [brand_id, amount_minor, currency]
              properties:
                brand_id:
                  type: string
                  description: A `brand_id` from GET /v1/brands.
                amount_minor:
                  type: integer
                  minimum: 1
                  description: Value in integer minor units (cents). 2500 = $25.00.
                currency: {type: string, default: USD}
                country: {type: string, description: ISO-3166-1 alpha-2; defaults to the key's region.}
                client_ref:
                  type: string
                  description: Agent-supplied idempotency key. Re-use returns the existing task.
                metadata:
                  type: object
                  additionalProperties: true
                  description: Opaque key/values echoed back on the task and webhook.
            example:
              brand_id: amazon_com-usa
              amount_minor: 2500
              currency: USD
              client_ref: reward-9f3a
      responses:
        "202":
          description: Accepted. Complete via webhook or polling.
          content:
            application/json:
              schema:
                type: object
                required: [task_id, status, state]
                properties:
                  task_id: {type: string, format: uuid}
                  status: {type: string, enum: [accepted]}
                  state:
                    type: string
                    description: State at accept time; `completed` means the code is already releasable.
                  estimated_seconds:
                    type: integer
                    description: 0 when already completed; otherwise poll GET /v1/purchases/{task_id} after this many seconds.
              example: {task_id: "3f1c...", status: accepted, state: paid, estimated_seconds: 8}
        "401": {$ref: "#/components/responses/Error"}
        "402": {$ref: "#/components/responses/Error"}
        "409": {$ref: "#/components/responses/Error"}
        "429": {$ref: "#/components/responses/RateLimited"}
        default: {$ref: "#/components/responses/Error"}
  /v1/purchases/{task_id}:
    get:
      operationId: get_purchase
      tags: [Purchases]
      summary: Poll purchase status
      parameters:
        - {name: task_id, in: path, required: true, schema: {type: string, format: uuid}}
      responses:
        "200":
          description: Current purchase state.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Purchase"}
        "404": {$ref: "#/components/responses/Error"}
        default: {$ref: "#/components/responses/Error"}
  /v1/purchases/{task_id}/code:
    post:
      operationId: get_code
      x-mcp-tool: get_code
      tags: [Purchases]
      summary: Release the redemption code (single release, logged)
      description: |
        Releases the code, link, or PIN to the purchasing agent exactly once and logs the access.
        A second call returns `ALREADY_RELEASED`. Only the purchasing agent (or its owner) may call it.
        Raw codes are never written to logs or traces.
      parameters:
        - {name: task_id, in: path, required: true, schema: {type: string, format: uuid}}
      responses:
        "200":
          description: Code released exactly once.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/CodeRelease"}
              example:
                code: "GIFT-XXXX-9F2A"
                face_minor: 2500
                remaining_minor: 2500
        "403": {$ref: "#/components/responses/Error"}
        "404": {$ref: "#/components/responses/Error"}
        "409": {$ref: "#/components/responses/Error"}
        default: {$ref: "#/components/responses/Error"}
  /v1/balance:
    get:
      operationId: check_balance
      x-mcp-tool: check_balance
      tags: [Account]
      summary: The calling agent's budget and platform status
      responses:
        "200":
          description: Balance snapshot.
          content:
            application/json:
              schema:
                type: object
                properties:
                  budget_remaining_minor: {type: integer}
                  daily_limit_minor: {type: integer}
                  monthly_limit_minor: {type: integer}
                  float_days_cover: {type: number}
                  kill_switch: {type: string, enum: [active, paused]}
              example:
                budget_remaining_minor: 42500
                daily_limit_minor: 50000
                monthly_limit_minor: 500000
                float_days_cover: 2.4
                kill_switch: active
        default: {$ref: "#/components/responses/Error"}
  /v1/approvals:
    post:
      operationId: request_approval
      x-mcp-tool: request_approval
      tags: [Approvals]
      summary: Re-ping the human approver for a parked task
      description: Rate-limited to one request per task per hour.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [task_id, reason]
              properties:
                task_id: {type: string, format: uuid}
                reason: {type: string, maxLength: 500}
            example: {task_id: "3f1c...", reason: "Customer waiting on refund"}
      responses:
        "200":
          description: Approval state.
          content:
            application/json:
              schema:
                type: object
                properties:
                  approval_id: {type: string, format: uuid}
                  status: {type: string, enum: [pending, approved, rejected, expired]}
        "404": {$ref: "#/components/responses/Error"}
        "429": {$ref: "#/components/responses/RateLimited"}
        default: {$ref: "#/components/responses/Error"}
  /v1/webhooks/bitrefill:
    post:
      operationId: bitrefill_webhook
      tags: [Webhooks]
      summary: Inbound provider webhook (HMAC-verified, event-id deduplicated)
      security: []
      parameters:
        - name: X-Bitrefill-Signature
          in: header
          required: true
          description: HMAC-SHA256 of the raw body keyed by the provider webhook secret; verified before processing.
          schema: {type: string}
      responses:
        "204": {description: Accepted.}
        "401": {description: Signature verification failed.}
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: Per-agent API key. Create one in the dashboard; scope it to spend caps and a brand allowlist.
  responses:
    Error:
      description: A taxonomy error. Branch on `code`; act on `hint`.
      content:
        application/json:
          schema: {$ref: "#/components/schemas/Error"}
          examples:
            budget_exceeded:
              value: {code: BUDGET_EXCEEDED, message: "Over the daily cap.", hint: "Lower amount_minor or wait for the daily reset.", task_id: "3f1c..."}
            approval_required:
              value: {code: APPROVAL_REQUIRED, message: "Purchase parked for human approval.", hint: "Call request_approval or await the purchase.completed webhook.", task_id: "3f1c..."}
            already_released:
              value: {code: ALREADY_RELEASED, message: "Code already released for this task.", hint: "The code was returned once; store it on first release.", task_id: "3f1c..."}
    RateLimited:
      description: Too many requests.
      headers:
        Retry-After:
          description: Seconds to wait before retrying.
          schema: {type: integer}
      content:
        application/json:
          schema: {$ref: "#/components/schemas/Error"}
          example: {code: RETRYABLE, message: "Rate limited.", hint: "Retry after retry_after seconds.", retry_after: 30}
  schemas:
    Brand:
      type: object
      properties:
        brand_id: {type: string, description: Stable id to pass to buy_giftcard.}
        name: {type: string}
        country: {type: string}
        currency: {type: string}
        denominations_minor: {type: array, items: {type: integer}, description: Fixed denominations, if any (minor units).}
        min_minor: {type: integer}
        max_minor: {type: integer}
        in_stock: {type: boolean}
    Purchase:
      type: object
      properties:
        task_id: {type: string, format: uuid}
        state:
          type: string
          description: Terminal states are `completed`, `abandoned`, `dlq`, `policy_denied`.
          enum: [created, awaiting_approval, invoice_pending, invoiced, paid, completed, abandoned, dlq, policy_denied]
        brand_id: {type: string}
        amount_minor: {type: integer}
        currency: {type: string}
        invoice_id: {type: string}
        code_available: {type: boolean, description: When true, call get_code.}
        created_at: {type: string, format: date-time}
    CodeRelease:
      type: object
      required: [code, face_minor, remaining_minor]
      description: The redemption value — code, link, or PIN, whichever the brand issues — in `code`.
      properties:
        code: {type: string}
        face_minor: {type: integer}
        remaining_minor: {type: integer}
    Error:
      type: object
      required: [code, message]
      properties:
        code:
          type: string
          description: Closed taxonomy. Agents branch on this value.
          enum: [POLICY_DENIED, BUDGET_EXCEEDED, APPROVAL_REQUIRED, APPROVAL_REJECTED,
                 OUT_OF_STOCK, PAYMENT_FAILED, INVOICE_EXPIRED, PROVIDER_HOLD,
                 ALREADY_RELEASED, NOT_FOUND, UNAUTHORIZED, RETRYABLE]
        message: {type: string, description: Human-readable summary.}
        hint: {type: string, description: Agent-actionable next step.}
        task_id: {type: string, format: uuid}
        retry_after: {type: integer, description: Seconds; present when code=RETRYABLE.}
webhooks:
  purchaseCompleted:
    post:
      tags: [Webhooks]
      summary: Outbound event to your registered webhook URL
      description: |
        Signed with your agent webhook secret. Verify `X-Giftcard-Signature`
        (HMAC-SHA256 of the raw body) before trusting. `event_id` is stable for
        at-least-once delivery, so dedupe on it.
      parameters:
        - name: X-Giftcard-Signature
          in: header
          required: true
          schema: {type: string}
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [event_id, event_type, task_id]
              properties:
                event_id: {type: string}
                event_type: {type: string, enum: [purchase.completed, purchase.failed]}
                task_id: {type: string, format: uuid}
                state: {type: string}
                created: {type: string, format: date-time}
            example:
              event_id: evt_7c1e
              event_type: purchase.completed
              task_id: "3f1c..."
              state: completed
              created: "2026-07-07T12:00:00Z"
      responses:
        "200": {description: Acknowledged.}
