openapi: 3.1.0
info:
  title: Fanback API
  version: 1.0.0
  description: Cashback for crypto cards, paid for by Platforms. Guide at https://fanback.net/docs/
servers:
  - url: https://api.fanback.net/v1
security:
  - apiKey: []
paths:
  /transactions:
    post:
      summary: Send a settled card transaction
      operationId: createTransaction
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/TransactionInput' }
      responses:
        '200': { description: Repeat of a transaction already recorded, content: { application/json: { schema: { $ref: '#/components/schemas/Transaction' } } } }
        '201': { description: Recorded, content: { application/json: { schema: { $ref: '#/components/schemas/Transaction' } } } }
        '400': { $ref: '#/components/responses/Error' }
  /cardholders/{cardholder_id}/balances:
    get:
      summary: Cashback balance per Platform for one cardholder
      operationId: getBalances
      parameters:
        - { name: cardholder_id, in: path, required: true, schema: { type: string } }
      responses:
        '200':
          description: Balances for Platforms open in the cardholder's country
          content:
            application/json:
              schema:
                type: object
                required: [cardholder_id, balances]
                properties:
                  cardholder_id: { type: string }
                  balances: { type: array, items: { $ref: '#/components/schemas/Balance' } }
  /platforms:
    get:
      summary: Platforms on your program
      operationId: listPlatforms
      parameters:
        - { name: country, in: query, required: false, description: Two-letter country; only Platforms open there, schema: { type: string } }
      responses:
        '200':
          description: Platforms
          content:
            application/json:
              schema:
                type: object
                required: [platforms]
                properties:
                  platforms: { type: array, items: { $ref: '#/components/schemas/Platform' } }
  /claims:
    post:
      summary: Claim a balance into the cardholder's Platform account
      operationId: createClaim
      parameters:
        - { name: Idempotency-Key, in: header, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ClaimInput' }
      responses:
        '201': { description: Claim created, content: { application/json: { schema: { $ref: '#/components/schemas/Claim' } } } }
        '400': { $ref: '#/components/responses/Error' }
        '409': { $ref: '#/components/responses/Error' }
    get:
      summary: A cardholder's claims, newest first
      operationId: listClaims
      parameters:
        - { name: cardholder_id, in: query, required: true, schema: { type: string } }
        - { name: cursor, in: query, required: false, schema: { type: string } }
      responses:
        '200':
          description: Up to 50 claims
          content:
            application/json:
              schema:
                type: object
                required: [claims, next_cursor]
                properties:
                  claims: { type: array, items: { $ref: '#/components/schemas/Claim' } }
                  next_cursor: { type: [string, 'null'] }
  /claims/{id}:
    get:
      summary: One claim
      operationId: getClaim
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        '200': { description: Claim, content: { application/json: { schema: { $ref: '#/components/schemas/Claim' } } } }
        '404': { $ref: '#/components/responses/Error' }
  /claims/{id}/resubmit:
    post:
      summary: Fix the username on an action_needed claim and send it again
      operationId: resubmitClaim
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [username]
              properties:
                username: { type: string }
      responses:
        '200': { description: Claim back to pending, content: { application/json: { schema: { $ref: '#/components/schemas/Claim' } } } }
        '409': { $ref: '#/components/responses/Error' }
webhooks:
  claim.updated:
    post:
      operationId: claimUpdated
      summary: A claim changed status
      description: Signed with the Fanback-Signature header (t=timestamp,v1=HMAC-SHA256 hex of "{t}.{raw body}"). Retried for 24 hours until you answer 2xx.
      parameters:
        - { name: Fanback-Signature, in: header, required: true, schema: { type: string } }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [id, type, created_at, data]
              properties:
                id: { type: string, description: Event ID; use it to skip repeats }
                type: { type: string, const: claim.updated }
                created_at: { type: string, format: date-time }
                data: { $ref: '#/components/schemas/Claim' }
      responses:
        '200': { description: Received }
components:
  securitySchemes:
    apiKey: { type: http, scheme: bearer }
  responses:
    Error:
      description: Error
      content:
        application/json:
          schema:
            type: object
            required: [error]
            properties:
              error:
                type: object
                required: [code, message]
                properties:
                  code:
                    type: string
                    enum: [invalid_request, not_found, below_minimum, insufficient_balance, restricted_country, claim_in_progress, not_eligible, not_resubmittable]
                  message: { type: string }
  schemas:
    Money:
      type: string
      pattern: '^-?\d+\.\d{2}$'
      description: USD amount with two decimals
      examples: ['12.40']
    TransactionInput:
      type: object
      required: [id, cardholder_id, cardholder_country, type, amount, mcc, settled_at]
      properties:
        id: { type: string, description: Your transaction ID; repeats are ignored }
        cardholder_id: { type: string, description: Your stable ID for the cardholder; no personal data }
        cardholder_country: { type: string, description: Two-letter KYC country }
        type: { type: string, enum: [purchase, refund] }
        original_transaction_id: { type: string, description: Required for refund }
        amount: { $ref: '#/components/schemas/Money' }
        mcc: { type: string, description: Merchant category code }
        settled_at: { type: string, format: date-time }
    Transaction:
      type: object
      required: [id, cashback]
      properties:
        id: { type: string }
        cashback:
          type: array
          items:
            type: object
            required: [platform, amount]
            properties:
              platform: { type: string }
              amount: { $ref: '#/components/schemas/Money' }
    Balance:
      type: object
      required: [platform, available, claimable, expiring]
      properties:
        platform: { type: string }
        available: { $ref: '#/components/schemas/Money' }
        claimable: { type: boolean, description: False below the Platform's minimum claim }
        expiring:
          type: [object, 'null']
          description: The next amount to expire
          required: [amount, expires_at]
          properties:
            amount: { $ref: '#/components/schemas/Money' }
            expires_at: { type: string, format: date-time }
    Platform:
      type: object
      required: [id, name, category, logo_url, signup_url, min_claim, payout_currencies]
      properties:
        id: { type: string }
        name: { type: string }
        category: { type: string, enum: [exchange, perps, trading_app, terminal, prop_firm, casino] }
        logo_url: { type: string, format: uri }
        signup_url: { type: string, format: uri }
        min_claim: { $ref: '#/components/schemas/Money' }
        payout_currencies: { type: array, items: { type: string } }
    ClaimInput:
      type: object
      required: [cardholder_id, platform, username, amount, payout_currency, cardholder_ip, terms_accepted]
      properties:
        cardholder_id: { type: string }
        platform: { type: string }
        username: { type: string, description: The cardholder's username on the Platform }
        amount: { $ref: '#/components/schemas/Money' }
        payout_currency: { type: string }
        cardholder_ip: { type: string, description: IP the cardholder claimed from }
        terms_accepted: { type: boolean, const: true }
    Claim:
      type: object
      required: [id, cardholder_id, platform, username, amount, payout_currency, status, message, created_at]
      properties:
        id: { type: string }
        cardholder_id: { type: string }
        platform: { type: string }
        username: { type: string }
        amount: { $ref: '#/components/schemas/Money' }
        payout_currency: { type: string }
        status: { type: string, enum: [pending, sent, paid, action_needed, returned] }
        message: { type: [string, 'null'], description: Text to show the cardholder for action_needed or returned }
        created_at: { type: string, format: date-time }
