openapi: 3.0.3
info:
  title: Cardmaker API
  version: 1.0.0
  description: |
    REST API for issuing and managing virtual cards.
    Give this file to a code-generation tool or AI agent to build a typed client.
    Store the API key in an environment variable; never hard-code it in source code.
servers:
  - url: /v1
    description: Current service origin
security:
  - bearerAuth: []
tags:
  - name: Account
  - name: Cards
  - name: Transactions
  - name: Events
  - name: Webhooks
paths:
  /webhooks:
    get:
      tags: [Webhooks]
      summary: List active webhooks
      responses:
        '200': { description: Active subscriptions, content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: '#/components/schemas/Webhook' } } } } } } }
  /webhooks/{id}/deliveries:
    parameters:
      - { name: id, in: path, required: true, schema: { type: string } }
    get:
      tags: [Webhooks]
      summary: List delivery attempts
      parameters:
        - { $ref: '#/components/parameters/Limit' }
        - { $ref: '#/components/parameters/Offset' }
      responses:
        '200': { description: Delivery history, content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: '#/components/schemas/WebhookDelivery' } }, total: { type: integer } } } } } }
        '404': { description: Subscription not found }
  /me:
    get:
      tags: [Account]
      summary: Get current account
      operationId: getCurrentAccount
      responses:
        '200': { description: Current account, content: { application/json: { schema: { $ref: '#/components/schemas/UserEnvelope' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /bins:
    get:
      tags: [Cards]
      summary: List available card products
      operationId: listBins
      responses:
        '200': { description: Available BINs, content: { application/json: { schema: { type: object, required: [data], properties: { data: { type: array, items: { $ref: '#/components/schemas/Bin' } } } } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /cards:
    get:
      tags: [Cards]
      summary: List cards
      operationId: listCards
      parameters:
        - { $ref: '#/components/parameters/Limit' }
        - { $ref: '#/components/parameters/Offset' }
      responses:
        '200': { description: Paginated cards, content: { application/json: { schema: { $ref: '#/components/schemas/CardList' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
    post:
      tags: [Cards]
      summary: Issue a virtual card
      operationId: issueCard
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/IssueCardRequest' } } } }
      responses:
        '200': { description: Issued card, content: { application/json: { schema: { $ref: '#/components/schemas/CardEnvelope' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/InsufficientBalance' }
        '502': { $ref: '#/components/responses/ServiceUnavailable' }
  /cards/{id}:
    get:
      tags: [Cards]
      summary: Get a card
      operationId: getCard
      parameters: [{ $ref: '#/components/parameters/CardId' }]
      responses:
        '200': { description: Card state, content: { application/json: { schema: { $ref: '#/components/schemas/CardEnvelope' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
  /cards/{id}/details:
    get:
      tags: [Cards]
      summary: Get sensitive card details
      operationId: getCardDetails
      description: Returns PAN, CVV and expiry. Limited to 20 requests per minute.
      parameters: [{ $ref: '#/components/parameters/CardId' }]
      responses:
        '200': { description: Sensitive card details, content: { application/json: { schema: { $ref: '#/components/schemas/CardDetailsEnvelope' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /cards/{id}/topup:
    post:
      tags: [Cards]
      summary: Add USD to a card
      operationId: topupCard
      parameters: [{ $ref: '#/components/parameters/CardId' }]
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/TopupRequest' } } } }
      responses:
        '200': { description: Updated card, content: { application/json: { schema: { $ref: '#/components/schemas/CardEnvelope' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '402': { $ref: '#/components/responses/InsufficientBalance' }
        '404': { $ref: '#/components/responses/NotFound' }
        '502': { $ref: '#/components/responses/ServiceUnavailable' }
  /cards/{id}/transactions:
    get:
      tags: [Transactions]
      summary: List card transactions
      operationId: listCardTransactions
      parameters: [{ $ref: '#/components/parameters/CardId' }, { $ref: '#/components/parameters/Limit' }, { $ref: '#/components/parameters/Offset' }]
      responses:
        '200': { description: Paginated transactions, content: { application/json: { schema: { type: object, required: [data, total], properties: { data: { type: array, items: { $ref: '#/components/schemas/Transaction' } }, total: { type: integer } } } } } }
        '404': { $ref: '#/components/responses/NotFound' }
  /cards/{id}/events:
    get:
      tags: [Events]
      summary: List card events including 3DS
      operationId: listCardEvents
      parameters: [{ $ref: '#/components/parameters/CardId' }, { $ref: '#/components/parameters/Limit' }, { $ref: '#/components/parameters/Offset' }]
      responses:
        '200': { description: Paginated events, content: { application/json: { schema: { type: object, required: [data, total], properties: { data: { type: array, items: { $ref: '#/components/schemas/CardEvent' } }, total: { type: integer } } } } } }
        '404': { $ref: '#/components/responses/NotFound' }
components:
  securitySchemes:
    bearerAuth: { type: http, scheme: bearer, bearerFormat: API key, description: 'API key with cmk_live_ prefix' }
  parameters:
    CardId: { name: id, in: path, required: true, schema: { type: string, format: uuid } }
    Limit: { name: limit, in: query, schema: { type: integer, default: 50, minimum: 1, maximum: 100 } }
    Offset: { name: offset, in: query, schema: { type: integer, default: 0, minimum: 0 } }
  responses:
    BadRequest: { description: Invalid request, content: { application/json: { schema: { $ref: '#/components/schemas/Error' }, example: { error: Bad Request } } } }
    Unauthorized: { description: Missing, invalid or revoked API key, content: { application/json: { schema: { $ref: '#/components/schemas/Error' }, example: { error: Unauthorized } } } }
    InsufficientBalance: { description: Account balance is insufficient, content: { application/json: { schema: { $ref: '#/components/schemas/Error' }, example: { error: insufficient_balance } } } }
    NotFound: { description: Resource not found, content: { application/json: { schema: { $ref: '#/components/schemas/Error' }, example: { error: Not Found } } } }
    Conflict: { description: Resource is not ready or state conflicts, content: { application/json: { schema: { $ref: '#/components/schemas/Error' }, example: { error: card details are not available yet } } } }
    RateLimited: { description: Rate limit exceeded, content: { application/json: { schema: { $ref: '#/components/schemas/Error' }, example: { error: rate_limited } } } }
    ServiceUnavailable: { description: Service temporarily unavailable, content: { application/json: { schema: { $ref: '#/components/schemas/Error' }, example: { error: service_unavailable } } } }
  schemas:
    WebhookEventType:
      type: string
      enum: [card.issued, card.frozen, card.unfrozen, card.closed, card.topup.succeeded, card.transaction.posted, card.transaction.declined, card.3ds.required, card.3ds.resolved]
    Webhook:
      type: object
      required: [id, url, events, active, created_at]
      properties:
        id: { type: string }
        url: { type: string, format: uri }
        events: { type: array, items: { $ref: '#/components/schemas/WebhookEventType' } }
        active: { type: boolean }
        signing_secret: { type: string, description: Returned only at creation }
        created_at: { type: integer, format: int64 }
    WebhookDelivery:
      type: object
      properties:
        id: { type: string }
        event_id: { type: string }
        event_type: { $ref: '#/components/schemas/WebhookEventType' }
        status: { type: string, enum: [pending, delivered, failed, cancelled] }
        attempts: { type: integer }
        status_code: { type: integer }
        error: { type: string }
        next_attempt_at: { type: integer, format: int64 }
        delivered_at: { type: integer, format: int64, nullable: true }
        created_at: { type: integer, format: int64 }
    WebhookPayload:
      type: object
      description: |
        Posted to the registered URL after local processing commits. JSON timestamps
        are Unix milliseconds. X-Cardmaker-Signature is lowercase hex MD5 (32 characters).
        Hash the exact raw body bytes followed by the UTF-8 bytes of signing_secret.
        Acknowledge with 2xx. Delivery is
        at least once; deduplicate by id. Up to 12 attempts within 24 hours. Delivery logs are retained for 30 days. Order is not guaranteed. Card state is rebuilt at send time and can change on retries.
      required: [id, type, created_at, processed_at, data]
      properties:
        id: { type: string, description: Local Cardmaker event ID }
        type: { $ref: '#/components/schemas/WebhookEventType' }
        created_at: { type: integer, format: int64 }
        processed_at: { type: integer, format: int64 }
        data:
          type: object
          required: [card, event]
          properties:
            card: { $ref: '#/components/schemas/Card' }
            event: { $ref: '#/components/schemas/CardEvent' }
            topup:
              type: object
              description: Included for a matched successful Cardmaker topup
              properties:
                id: { type: string }
                card_id: { type: string }
                amount_usd: { type: number }
                status: { type: string, enum: [succeeded] }
                client_request_id: { type: string, maxLength: 128, description: Unique operation identifier; reuse for retries }
    Error: { type: object, required: [error], properties: { error: { type: string } } }
    User: { type: object, required: [balance_usd, disabled], properties: { id: { type: string, format: uuid }, username: { type: string }, role: { type: string }, balance_usd: { type: number, format: double }, disabled: { type: boolean }, created_at: { type: integer, format: int64 }, updated_at: { type: integer, format: int64 } } }
    UserEnvelope: { type: object, required: [data], properties: { data: { $ref: '#/components/schemas/User' } } }
    Bin: { type: object, required: [bin, network, country, currency, min_amount, max_amount, available, apple_pay, google_pay, stripe], properties: { bin: { type: string }, network: { type: string }, country: { type: string }, currency: { type: string }, min_amount: { type: number }, max_amount: { type: number }, available: { type: boolean }, apple_pay: { type: boolean }, google_pay: { type: boolean }, stripe: { type: boolean } } }
    IssueCardRequest: { type: object, required: [amount_usd, client_request_id], properties: { amount_usd: { type: number, minimum: 20, maximum: 1000000 }, network: { type: string, enum: [visa, mastercard] }, holder_name: { type: string, maxLength: 200 }, holder_email: { type: string, format: email, maxLength: 200 }, spending_limit_usd: { type: number, minimum: 0, maximum: 1000000 }, merchant_blocklist: { type: array, items: { type: string } }, metadata: { type: object, additionalProperties: true }, client_request_id: { type: string, maxLength: 128, description: Unique operation identifier; reuse for retries } } }
    TopupRequest: { type: object, required: [amount_usd, client_request_id], properties: { amount_usd: { type: number, minimum: 20, maximum: 1000000 }, client_request_id: { type: string, maxLength: 128, description: Unique operation identifier; reuse for retries } } }
    Card: { type: object, required: [id, status, amount_usd, total_loaded_usd, balance_usd, created_at], properties: { id: { type: string, format: uuid }, status: { type: string, example: active }, network: { type: string }, last4: { type: string }, amount_usd: { type: number }, total_loaded_usd: { type: number }, balance_usd: { type: number }, spending_limit_usd: { type: number, nullable: true }, holder_name: { type: string, maxLength: 200 }, holder_email: { type: string }, merchant_blocklist: { type: array, items: { type: string } }, created_at: { type: integer, format: int64 } } }
    CardEnvelope: { type: object, required: [data], properties: { data: { $ref: '#/components/schemas/Card' } } }
    CardList: { type: object, required: [data, total], properties: { data: { type: array, items: { $ref: '#/components/schemas/Card' } }, total: { type: integer } } }
    CardDetails: { type: object, required: [id, number, cvv, expiry], properties: { id: { type: string, format: uuid }, number: { type: string }, cvv: { type: string }, expiry: { type: string }, holder_name: { type: string, maxLength: 200 }, holder_email: { type: string }, billing_address: { type: object, additionalProperties: true } } }
    CardDetailsEnvelope: { type: object, required: [data], properties: { data: { $ref: '#/components/schemas/CardDetails' } } }
    Transaction: { type: object, required: [id, card_id, type, status, amount_usd, created_at], properties: { id: { type: string }, card_id: { type: string }, type: { type: string }, status: { type: string }, amount_usd: { type: number }, currency: { type: string }, merchant: { type: string }, merchant_category: { type: string }, decline_reason: { type: string }, posted_at: { type: integer, format: int64, nullable: true }, created_at: { type: integer, format: int64 } } }
    CardEvent: { type: object, required: [id, card_id, type, created_at], properties: { id: { type: string }, card_id: { type: string }, type: { type: string }, otp_code: { type: string }, expires_at: { type: integer, format: int64, nullable: true }, amount: { type: number, nullable: true }, merchant: { type: string }, currency: { type: string }, created_at: { type: integer, format: int64 } } }
