openapi: 3.1.0
info:
  title: Valescrow / TapPay API
  version: "1.1.0"
  description: >
    Programmable money infrastructure: conditional escrow (allocations), payment
    requests, refunds, payouts, balances, and webhooks. Authenticate with a
    TapPay API key (`tap_live_sk_...` / `tap_test_sk_...`) as a Bearer token.

    Institutional (Valescrow) integrations may use the `https://valescrow.com/api/v1`
    server; both serve the identical API.
  contact:
    name: Valescrow Developer Support
    url: https://usetappay.app/developers
  license:
    name: Proprietary

servers:
  - url: https://usetappay.app/api/v1
    description: Production (TapPay-branded host)
  - url: https://valescrow.com/api/v1
    description: Production (Valescrow-branded host — identical API)

security:
  - bearerAuth: []

tags:
  - name: Allocations
    description: Conditional escrow — create, fund, approve, trigger, release.
  - name: Payments
    description: Payment requests, verification, transactions, refunds.
  - name: Treasury
    description: Balances and bank payouts.
  - name: Webhooks
    description: Event subscription management.
  - name: Keys
    description: API key management (JWT-authenticated; not API-key callable).

paths:
  # ── Allocations / escrow ─────────────────────────────────────────
  /allocations:
    post:
      tags: [Allocations]
      summary: Create a conditional allocation (escrow)
      description: Requires the `escrow:write` scope and a merchant/organization account.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateAllocationRequest" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CreateAllocationResponse" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
    get:
      tags: [Allocations]
      summary: List allocations
      description: Requires the `escrow:read` scope.
      parameters:
        - { name: status, in: query, schema: { type: string } }
        - { name: limit, in: query, schema: { type: integer, default: 20 } }
        - { name: offset, in: query, schema: { type: integer, default: 0 } }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AllocationList" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /allocations/{id}:
    get:
      tags: [Allocations]
      summary: Get allocation detail
      description: Requires the `escrow:read` scope. Secrets (milestone_secret, inspector emails) are scrubbed.
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Allocation" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /allocations/fund:
    post:
      tags: [Allocations]
      summary: Fund an allocation with an on-chain transaction hash
      description: Requires the `escrow:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/FundAllocationRequest" }
      responses:
        "200": { description: OK }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /allocations/approve:
    post:
      tags: [Allocations]
      summary: Submit an approval for a manual_approval condition
      description: Requires the `escrow:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ApproveAllocationRequest" }
      responses:
        "200": { description: OK }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /allocations/cancel:
    post:
      tags: [Allocations]
      summary: Cancel an allocation (refunds if funded)
      description: Requires the `escrow:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [allocation_id]
              properties:
                allocation_id: { type: string }
                reason: { type: string }
      responses:
        "200": { description: OK }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /allocations/milestone:
    post:
      tags: [Allocations]
      summary: Trigger a milestone condition (HMAC-signed)
      description: Requires the `escrow:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/TriggerMilestoneRequest" }
      responses:
        "200": { description: OK }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /allocations/metric:
    post:
      tags: [Allocations]
      summary: Push a metric to a milestone with a metric inspector
      description: Requires the `escrow:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [milestone_key, timestamp, signature, value]
              properties:
                milestone_key: { type: string }
                timestamp: { type: integer }
                signature: { type: string }
                value: { type: number }
                metadata: { type: object, additionalProperties: true }
      responses:
        "200": { description: OK }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /allocations/public:
    get:
      tags: [Allocations]
      summary: List publicly discoverable allocations
      description: >
        No authentication required. Returns public-safe data only — zero wallet
        addresses, secrets, or inspector emails. IP rate-limited.
      security: []
      parameters:
        - { name: category, in: query, schema: { type: string } }
        - { name: currency, in: query, schema: { type: string } }
        - { name: status, in: query, schema: { type: string } }
        - { name: id, in: query, schema: { type: string } }
        - { name: limit, in: query, schema: { type: integer } }
        - { name: offset, in: query, schema: { type: integer } }
      responses:
        "200": { description: OK }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Payments ─────────────────────────────────────────────────────
  /payment-requests:
    post:
      tags: [Payments]
      summary: Create a payment request
      description: Requires the `payments:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [amount, currency]
              properties:
                amount: { type: number }
                currency: { type: string }
                description: { type: string }
                reference: { type: string }
                splits:
                  type: array
                  maxItems: 5
                  items:
                    type: object
                    properties:
                      wallet: { type: string }
                      amount: { type: number }
      responses:
        "201": { description: Created }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    get:
      tags: [Payments]
      summary: List payment requests
      description: Requires the `payments:read` scope.
      responses:
        "200": { description: OK }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /payment-requests/{id}:
    get:
      tags: [Payments]
      summary: Get a payment request
      description: Requires the `payments:read` scope.
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200": { description: OK }
        "404": { $ref: "#/components/responses/NotFound" }

  /verify/{reference}:
    get:
      tags: [Payments]
      summary: Verify a payment by reference
      description: Requires the `payments:read` scope.
      parameters:
        - { name: reference, in: path, required: true, schema: { type: string } }
      responses:
        "200": { description: OK }
        "404": { $ref: "#/components/responses/NotFound" }

  /transactions:
    get:
      tags: [Payments]
      summary: List transactions
      description: Requires the `payments:read` scope.
      responses:
        "200": { description: OK }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /refunds:
    post:
      tags: [Payments]
      summary: Create a refund (full or partial)
      description: Requires the `refunds:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [payment_reference]
              properties:
                payment_reference: { type: string }
                amount: { type: number, description: "Omit for a full refund." }
                reason: { type: string }
      responses:
        "201": { description: Created }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    get:
      tags: [Payments]
      summary: List refunds
      description: Requires the `payments:read` scope.
      responses:
        "200": { description: OK }

  /refunds/{id}:
    get:
      tags: [Payments]
      summary: Get a refund
      description: Requires the `payments:read` scope.
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200": { description: OK }
        "404": { $ref: "#/components/responses/NotFound" }

  # ── Treasury ─────────────────────────────────────────────────────
  /balance:
    get:
      tags: [Treasury]
      summary: Get per-stablecoin balances
      description: Requires the `balance:read` scope.
      responses:
        "200": { description: OK }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /payouts:
    post:
      tags: [Treasury]
      summary: Initiate a bank payout
      description: Requires the `payouts:write` scope, a merchant/organization account, and a non-frozen account.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [amount, currency]
              properties:
                amount: { type: number }
                currency: { type: string }
                bank_account_number: { type: string }
                bank_routing_code: { type: string }
                bank_account_name: { type: string }
      responses:
        "200": { description: OK }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    get:
      tags: [Treasury]
      summary: List payouts
      description: Requires the `payouts:read` scope.
      responses:
        "200": { description: OK }

  # ── Webhooks ─────────────────────────────────────────────────────
  /webhooks:
    post:
      tags: [Webhooks]
      summary: Register a webhook endpoint
      description: Requires the `webhooks:manage` scope. Max 5 active webhooks.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url: { type: string, format: uri, description: "Must be HTTPS." }
                events:
                  type: array
                  items: { type: string }
                  description: "Subscribed event names. See the webhooks section for the catalog."
      responses:
        "201": { description: Created — returns the signing secret once. }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    get:
      tags: [Webhooks]
      summary: List webhooks
      description: Requires the `webhooks:manage` scope.
      responses:
        "200": { description: OK }
    delete:
      tags: [Webhooks]
      summary: Delete a webhook by id
      description: Requires the `webhooks:manage` scope.
      parameters:
        - { name: id, in: query, required: true, schema: { type: string } }
      responses:
        "200": { description: OK }

  # ── Keys (JWT-authenticated, not API-key callable) ───────────────
  /keys:
    get:
      tags: [Keys]
      summary: List API keys (JWT auth)
      description: >
        Called from the developer settings screen with a Supabase JWT, not an API
        key. Documented for completeness; not part of the API-key surface.
      security: []
      responses:
        "200": { description: OK }
    delete:
      tags: [Keys]
      summary: Revoke an API key (JWT auth)
      security: []
      parameters:
        - { name: id, in: query, required: true, schema: { type: string } }
      responses:
        "200": { description: OK }

# ── Webhook events (OpenAPI 3.1 top-level `webhooks`) ───────────────
webhooks:
  payment.received:
    post:
      summary: A payment request was paid.
      requestBody: { $ref: "#/components/requestBodies/WebhookDelivery" }
      responses: { "200": { description: Acknowledged } }
  payment.sent:
    post:
      summary: Reserved — not currently emitted.
      requestBody: { $ref: "#/components/requestBodies/WebhookDelivery" }
      responses: { "200": { description: Acknowledged } }
  payout.completed:
    post:
      summary: A bank payout settled successfully.
      requestBody: { $ref: "#/components/requestBodies/WebhookDelivery" }
      responses: { "200": { description: Acknowledged } }
  payout.failed:
    post:
      summary: A bank payout failed.
      requestBody: { $ref: "#/components/requestBodies/WebhookDelivery" }
      responses: { "200": { description: Acknowledged } }
  deposit.confirmed:
    post:
      summary: A virtual-account deposit was confirmed.
      requestBody: { $ref: "#/components/requestBodies/WebhookDelivery" }
      responses: { "200": { description: Acknowledged } }
  refund.completed:
    post:
      summary: A refund was transferred on-chain.
      requestBody: { $ref: "#/components/requestBodies/WebhookDelivery" }
      responses: { "200": { description: Acknowledged } }
  refund.failed:
    post:
      summary: A refund's on-chain transfer failed.
      requestBody: { $ref: "#/components/requestBodies/WebhookDelivery" }
      responses: { "200": { description: Acknowledged } }
  allocation.funded:
    post:
      summary: An allocation reached its funding goal and is fully escrowed.
      requestBody: { $ref: "#/components/requestBodies/WebhookDelivery" }
      responses: { "200": { description: Acknowledged } }
  allocation.contribution_received:
    post:
      summary: A multi-investor allocation received a contribution (goal not yet reached).
      requestBody: { $ref: "#/components/requestBodies/WebhookDelivery" }
      responses: { "200": { description: Acknowledged } }
  allocation.condition_met:
    post:
      summary: A single release condition was satisfied.
      requestBody: { $ref: "#/components/requestBodies/WebhookDelivery" }
      responses: { "200": { description: Acknowledged } }
  allocation.all_conditions_met:
    post:
      summary: All release conditions were satisfied; release is triggered.
      requestBody: { $ref: "#/components/requestBodies/WebhookDelivery" }
      responses: { "200": { description: Acknowledged } }
  allocation.released:
    post:
      summary: Funds were released/settled to recipients.
      requestBody: { $ref: "#/components/requestBodies/WebhookDelivery" }
      responses: { "200": { description: Acknowledged } }
  allocation.partial_failure:
    post:
      summary: Release completed but one or more recipient transfers failed.
      requestBody: { $ref: "#/components/requestBodies/WebhookDelivery" }
      responses: { "200": { description: Acknowledged } }
  allocation.cancelled:
    post:
      summary: An allocation was cancelled (and refunded if funded).
      requestBody: { $ref: "#/components/requestBodies/WebhookDelivery" }
      responses: { "200": { description: Acknowledged } }
  allocation.milestone_rejected:
    post:
      summary: A milestone condition was rejected by its inspector.
      requestBody: { $ref: "#/components/requestBodies/WebhookDelivery" }
      responses: { "200": { description: Acknowledged } }
  allocation.distribution_completed:
    post:
      summary: A recurring distribution run completed.
      requestBody: { $ref: "#/components/requestBodies/WebhookDelivery" }
      responses: { "200": { description: Acknowledged } }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: >
        TapPay API key as a Bearer token: `Authorization: Bearer tap_live_sk_...`
        (or `tap_test_sk_...` for the sandbox environment).

  responses:
    BadRequest:
      description: Validation error
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Unauthorized:
      description: Missing or invalid API key
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Forbidden:
      description: >
        The key lacks the required scope (`INSUFFICIENT_SCOPE`) or the account is
        not a merchant/organization (`MERCHANT_ACCOUNT_REQUIRED`).
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    RateLimited:
      description: Rate limit exceeded
      headers:
        X-RateLimit-Limit: { schema: { type: integer } }
        X-RateLimit-Remaining: { schema: { type: integer } }
        X-RateLimit-Reset: { schema: { type: integer } }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }

  requestBodies:
    WebhookDelivery:
      description: Signed event delivery (verify with the `tappay-signature` HMAC-SHA256 header).
      content:
        application/json:
          schema: { $ref: "#/components/schemas/WebhookDelivery" }

  schemas:
    Error:
      type: object
      properties:
        error: { type: string, description: Human-readable message }
        code:
          type: string
          description: Machine-readable error code (e.g. MERCHANT_ACCOUNT_REQUIRED, INSUFFICIENT_SCOPE)
      required: [error]

    WebhookDelivery:
      type: object
      description: Envelope POSTed to a subscribed endpoint.
      properties:
        delivery_id: { type: string }
        event: { type: string }
        created: { type: integer, description: Unix epoch milliseconds }
        data: { type: object, additionalProperties: true }
      required: [event, created, data]

    ConditionInput:
      type: object
      required: [type]
      properties:
        type:
          type: string
          enum: [immediate, manual_approval, time_lock, milestone, all_of, any_of]
        label: { type: string }
        required_approvals: { type: integer, description: "manual_approval: N of M" }
        approver_wallets:
          type: array
          items: { type: string }
        release_after: { type: string, format: date-time, description: "time_lock: earliest release" }
        on_expiry:
          type: string
          enum: [release, refund]
          description: "time_lock only — vesting (release, default) or escrow timeout (refund)."
        milestone_key: { type: string }
        milestone_tiers:
          type: array
          items:
            type: object
            properties:
              label: { type: string }
              release_percent: { type: number }
        inspector_email: { type: string, format: email }
        attestor_id:
          type: string
          description: "milestone: reference a registered attestor (fills inspector_email if omitted)."
        sub_condition_indices:
          type: array
          items: { type: integer }

    RecipientInput:
      type: object
      required: [amount]
      properties:
        wallet: { type: string, description: "0x address (stablecoin payout)" }
        amount: { type: number }
        label: { type: string }
        condition_index: { type: integer }
        bank_account_number: { type: string }
        bank_account_name: { type: string }
        bank_routing_code: { type: string }
        bank_country: { type: string }
        bank_name: { type: string }

    CreateAllocationRequest:
      type: object
      required: [title, total_amount, recipients, conditions]
      properties:
        title: { type: string, minLength: 2, maxLength: 200 }
        description: { type: string }
        reference: { type: string }
        category: { type: string }
        currency: { type: string, default: NGNm }
        total_amount: { type: number }
        funding_deadline: { type: string, format: date-time }
        recipients:
          type: array
          minItems: 1
          maxItems: 1000
          items: { $ref: "#/components/schemas/RecipientInput" }
        conditions:
          type: array
          minItems: 1
          maxItems: 20
          items: { $ref: "#/components/schemas/ConditionInput" }
        is_discoverable: { type: boolean }
        acknowledge_large_allocation: { type: boolean }

    CreateAllocationResponse:
      type: object
      properties:
        success: { type: boolean }
        allocation_id: { type: string }
        payment_url: { type: string }
        total_amount: { type: number }
        recipient_count: { type: integer }

    FundAllocationRequest:
      type: object
      required: [allocation_id, tx_hash]
      properties:
        allocation_id: { type: string }
        tx_hash: { type: string }
        investment_amount: { type: number, description: "Required for multi-investor allocations." }

    ApproveAllocationRequest:
      type: object
      required: [allocation_id, condition_id, approver_wallet]
      properties:
        allocation_id: { type: string }
        condition_id: { type: string }
        approver_wallet: { type: string }
        note: { type: string }

    TriggerMilestoneRequest:
      type: object
      required: [milestone_key, timestamp, signature]
      properties:
        milestone_key: { type: string }
        timestamp: { type: integer }
        signature: { type: string, description: "HMAC-SHA256 of `milestone_key:timestamp`." }
        action: { type: string, enum: [confirm, reject, pending] }
        tier_index: { type: integer }
        note: { type: string }
        evidence:
          type: object
          description: "Supporting evidence (recorded in the audit log + webhook; not part of the HMAC signature)."
          properties:
            url: { type: string, format: uri }
            sha256: { type: string, pattern: "^[a-f0-9]{64}$" }
            note: { type: string, maxLength: 2000 }

    Allocation:
      type: object
      description: Full allocation detail (secrets scrubbed). See the SDK types for the complete shape.
      additionalProperties: true

    AllocationList:
      type: object
      properties:
        allocations:
          type: array
          items: { type: object, additionalProperties: true }
        total: { type: integer }
        has_more: { type: boolean }
