openapi: 3.1.0
info:
  title: Payrelayer API
  version: "1.0"
  description: >
    Payrelayer meters AI-agent traffic into per-request USDC payments over the
    x402 protocol (HTTP 402). The two core endpoints — challenge and verify —
    are called by the seller SDKs (server-to-server), authenticated with the
    frozen X402v1 HMAC scheme. Standard x402 clients are supported: challenges
    include an `accepts[]` array and verify accepts a base64 `X-PAYMENT`
    payload. This spec is descriptive; treat it as accurate at time of writing.
  license:
    name: Proprietary
  contact:
    url: https://payrelayer.com
servers:
  - url: https://api.payrelayer.com
    description: Production
tags:
  - name: Protocol
    description: The x402 challenge/verify handshake (SDK ↔ platform, HMAC-authed).
  - name: Routes
    description: Merchant monetized-route management (dashboard session auth).
  - name: Keys
    description: Merchant API key management.
  - name: Webhooks
    description: Webhook endpoint configuration.
  - name: Balance
    description: Merchant balance and payout history.
paths:
  /api/v1/challenge:
    post:
      tags: [Protocol]
      summary: Issue an x402 payment challenge for a monetized route
      description: >
        Returns a 402-style challenge for a registered route. The body is both
        standard (`x402Version` + `accepts[]`) and legacy (flat keys) so both
        standard x402 clients and legacy Payrelayer SDKs work. Unregistered
        route → 404.
      security: [{ X402v1: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ChallengeRequest"
      responses:
        "200":
          description: Challenge issued
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ChallengeBody"
        "404":
          description: No such route
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Invalid or missing HMAC signature
  /api/v1/verify:
    post:
      tags: [Protocol]
      summary: Verify a payment and authorize a request
      description: >
        Verifies payment for a route. Legacy callers send the platform `nonce`
        plus a proof; standard x402 clients send only a standard `payment_proof`
        (the decoded `X-PAYMENT` envelope) with no nonce. On success returns
        `{allowed:true}`, and for standard payments a base64 `payment_response`
        to surface as the `X-PAYMENT-RESPONSE` header. Every non-allow outcome
        is HTTP 402 with a `reason`.
      security: [{ X402v1: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/VerifyRequest"
      responses:
        "200":
          description: Payment verified; request allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VerifyAllowed"
        "402":
          description: Not allowed (reason = no_such_route | bad_nonce | unpaid | replay)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VerifyDenied"
        "401":
          description: Invalid or missing HMAC signature
  /api/v1/merchant/routes:
    get:
      tags: [Routes]
      summary: List monetized routes
      security: [{ SessionAuth: [] }]
      responses:
        "200":
          description: Routes
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/MonetizedRoute" }
    post:
      tags: [Routes]
      summary: Create a monetized route
      security: [{ SessionAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MonetizedRouteInput"
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/MonetizedRoute" }
  /api/v1/merchant/routes/{route}:
    patch:
      tags: [Routes]
      summary: Update a monetized route
      security: [{ SessionAuth: [] }]
      parameters:
        - name: route
          in: path
          required: true
          schema: { type: integer }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MonetizedRouteInput"
      responses:
        "200":
          description: Updated
    delete:
      tags: [Routes]
      summary: Delete a monetized route
      security: [{ SessionAuth: [] }]
      parameters:
        - name: route
          in: path
          required: true
          schema: { type: integer }
      responses:
        "200":
          description: Deleted
  /api/v1/merchant/api-keys:
    get:
      tags: [Keys]
      summary: List API keys
      security: [{ SessionAuth: [] }]
      responses:
        "200":
          description: Keys
    post:
      tags: [Keys]
      summary: Create an API key (secret shown once)
      security: [{ SessionAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, mode]
              properties:
                name: { type: string }
                mode: { type: string, enum: [test, live] }
      responses:
        "201":
          description: Created
  /api/v1/merchant/webhooks:
    get:
      tags: [Webhooks]
      summary: List webhook endpoints
      security: [{ SessionAuth: [] }]
      responses:
        "200":
          description: Endpoints
    post:
      tags: [Webhooks]
      summary: Create a webhook endpoint (signing secret shown once)
      security: [{ SessionAuth: [] }]
      responses:
        "201":
          description: Created
  /api/v1/merchant/balance:
    get:
      tags: [Balance]
      summary: Merchant balance (settled + pending payout)
      security: [{ SessionAuth: [] }]
      responses:
        "200":
          description: Balance
  /api/v1/merchant/payouts:
    get:
      tags: [Balance]
      summary: Payout history
      security: [{ SessionAuth: [] }]
      responses:
        "200":
          description: Payouts
components:
  securitySchemes:
    X402v1:
      type: apiKey
      in: header
      name: X-X402-Signature
      description: >
        Frozen X402v1 HMAC scheme. Send X-X402-Key, X-X402-Timestamp,
        X-X402-Nonce, X-X402-Signature. Signature = HMAC-SHA256 of the canonical
        string "X402v1\nMETHOD\npath\ntimestamp\nnonce\nsha256hex(body)".
    SessionAuth:
      type: apiKey
      in: cookie
      name: payrelayer_session
      description: Dashboard session (Sanctum) for human/merchant UI calls.
  schemas:
    Error:
      type: object
      properties:
        error: { type: string, example: no_such_route }
    ChallengeRequest:
      type: object
      required: [route]
      properties:
        route: { type: string, example: /premium }
        method: { type: string, example: GET, default: GET }
    ChallengeBody:
      type: object
      description: Merged standard + legacy challenge.
      properties:
        x402Version: { type: integer, example: 1 }
        accepts:
          type: array
          items: { $ref: "#/components/schemas/PaymentRequirements" }
        paymentRequired: { type: boolean, example: true }
        amount: { type: string, example: "0.10" }
        currency: { type: string, example: USDC }
        network: { type: string, example: base }
        recipient: { type: string }
        resource: { type: string, example: /premium }
        nonce: { type: string }
        expiresAt: { type: string, format: date-time }
    PaymentRequirements:
      type: object
      properties:
        scheme: { type: string, example: exact }
        network: { type: string, example: base }
        asset: { type: string, example: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" }
        payTo: { type: string }
        maxAmountRequired: { type: string, description: Atomic units (USDC 6dp), example: "100000" }
        resource: { type: string }
        maxTimeoutSeconds: { type: integer, example: 600 }
    VerifyRequest:
      type: object
      required: [route]
      properties:
        route: { type: string, example: /premium }
        method: { type: string, example: GET }
        nonce:
          type: string
          description: Platform nonce (legacy flow). Omitted by standard x402 clients.
        payer: { type: string }
        payment_proof:
          type: object
          additionalProperties: true
          description: >
            Standard x402 payment payload (the decoded X-PAYMENT envelope) with
            x402Version set, or a legacy proof object.
    VerifyAllowed:
      type: object
      properties:
        allowed: { type: boolean, example: true }
        payment_response:
          type: string
          description: base64 attestation to surface as X-PAYMENT-RESPONSE (standard payments).
    VerifyDenied:
      type: object
      properties:
        allowed: { type: boolean, example: false }
        reason: { type: string, enum: [no_such_route, bad_nonce, unpaid, replay] }
    MonetizedRoute:
      type: object
      properties:
        id: { type: integer }
        method: { type: string, example: "*" }
        pattern: { type: string, example: /premium }
        price_cents: { type: integer, example: 10 }
        currency: { type: string, example: USDC }
        network: { type: string, example: base }
        active: { type: boolean }
        priority: { type: integer }
        discoverable: { type: boolean }
        discovery_description: { type: [string, "null"] }
        discovery_category: { type: [string, "null"] }
    MonetizedRouteInput:
      type: object
      required: [pattern, price_cents]
      properties:
        method: { type: string, example: "*" }
        pattern: { type: string, example: /premium }
        price_cents: { type: integer, minimum: 1, example: 10 }
        discoverable: { type: boolean }
        discovery_description: { type: string, maxLength: 200 }
