openapi: 3.1.0
info:
  title: Forward API
  version: "1.0.0"
  summary: Pay-per-result growth, operated by agents and built to be bought by agents.
  description: >
    Forward delivers customers — qualified leads and booked meetings — billed per verified
    result. This API lets an autonomous agent complete the entire purchase: discover the
    catalog, submit a brief, get a quote, authorize payment via Stripe Agentic Commerce
    with a hard budget cap, then poll the engagement and collect verified results. A
    result is only charged after it passes that product's acceptance criteria; charges are
    itemized per result, idempotent, and reversible on acceptance failure.
  contact:
    name: Forward (for agents)
    url: /agents.html
servers:
  - url: /api/v1
    description: Forward API v1
security:
  - bearerAuth: []
tags:
  - name: discovery
  - name: commerce
  - name: delivery
paths:
  /catalog:
    get:
      tags: [discovery]
      operationId: getCatalog
      summary: List products, acceptance criteria, and prices.
      security: []
      responses:
        "200":
          description: The Forward catalog.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Catalog" }
  /capabilities:
    get:
      tags: [discovery]
      operationId: getCapabilities
      summary: Report mode and which live providers are wired.
      security: []
      responses:
        "200":
          description: Capabilities.
          content:
            application/json:
              schema:
                type: object
                properties:
                  mode: { type: string, enum: [test, live] }
                  billing_live: { type: boolean }
                  research_live: { type: boolean }
                  email_live: { type: boolean }
                  calendar_live: { type: boolean }
                  cms_live: { type: boolean }
  /quote/calc:
    post:
      tags: [commerce]
      operationId: calcQuote
      summary: Price preview (no commitment, no key) — same engine as /quote.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [product, volume]
              properties:
                product: { type: string, enum: [leads, meetings, content, campaigns] }
                difficulty: { type: string, enum: [standard, hard, elite], default: standard }
                volume: { type: integer, minimum: 1, example: 50 }
      responses:
        "200":
          description: Computed price.
          content:
            application/json:
              schema:
                type: object
                properties:
                  product: { type: string }
                  difficulty: { type: string }
                  volume: { type: integer }
                  unit_price_usd: { type: number, example: 304 }
                  total_usd: { type: number, example: 15200 }
                  alt_total_usd: { type: number, example: 35000 }
                  savings_pct: { type: integer, example: 57 }
                  eta_days: { type: string, example: "14-30" }
  /keys:
    post:
      tags: [commerce]
      operationId: mintKey
      summary: Mint an API key for an account.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [account_name]
              properties:
                account_name: { type: string, example: "Acme" }
                mode:
                  type: string
                  enum: [live, test]
                  default: test
      responses:
        "200":
          description: A new API key.
          content:
            application/json:
              schema:
                type: object
                properties:
                  account_id: { type: string, example: "acct_8Kd2" }
                  api_key: { type: string, example: "fwd_test_9aF3xQ" }
                  mode: { type: string, example: "test" }
                  credits_usd: { type: number, example: 25, description: Free starter credits — an agent can transact immediately. }
  /credits:
    get:
      tags: [commerce]
      operationId: getCredits
      summary: Get the account's prepaid credit balance.
      responses:
        "200":
          description: Credit balance.
          content:
            application/json:
              schema:
                type: object
                properties:
                  credits_usd: { type: number, example: 25 }
                  currency: { type: string, example: "usd" }
                  mode: { type: string, example: "test" }
                  starter_credits: { type: number, example: 25 }
        "401": { $ref: "#/components/responses/Unauthorized" }
  /credits/topup:
    post:
      tags: [commerce]
      operationId: topupCredits
      summary: Add credits. In live mode, settle via x402 (X-PAYMENT header) or Stripe.
      parameters:
        - { name: X-PAYMENT, in: header, required: false, schema: { type: string }, description: x402 settlement proof. }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [amount_usd]
              properties:
                amount_usd: { type: number, minimum: 1, example: 100 }
      responses:
        "200":
          description: New balance.
          content:
            application/json:
              schema:
                type: object
                properties:
                  credits_usd: { type: number }
                  added_usd: { type: number }
                  settled_via: { type: string, example: "x402" }
        "402":
          description: Payment required — response body carries x402 instructions (accepts, facilitator).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "401": { $ref: "#/components/responses/Unauthorized" }
  /stats:
    get:
      tags: [discovery]
      operationId: getStats
      summary: Live public proof — verified results delivered, billed, acceptance rate.
      security: []
      responses:
        "200":
          description: Aggregate stats.
          content:
            application/json:
              schema:
                type: object
                properties:
                  results_delivered: { type: integer }
                  results_rejected: { type: integer }
                  acceptance_rate: { type: number, example: 0.86 }
                  billed_usd: { type: number }
                  engagements: { type: integer }
  /track/conversion:
    post:
      tags: [delivery]
      operationId: trackConversion
      summary: Report a real conversion from your site (Campaigns). Secured by track_token; duplicates are never double-billed.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [engagement_id, track_token]
              properties:
                engagement_id: { type: string }
                track_token: { type: string, description: Returned by checkout for campaigns engagements. }
                conversion_id: { type: string, description: Your idempotency key for this conversion (e.g. order id). }
                type: { type: string, example: "demo signup" }
                value_usd: { type: number }
                meta: { type: object }
      responses:
        "200":
          description: Tracked (or flagged duplicate).
          content:
            application/json:
              schema:
                type: object
                properties:
                  tracked: { type: boolean }
                  duplicate: { type: boolean }
                  conversion_id: { type: string }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/Invalid" }
  /brief:
    post:
      tags: [commerce]
      operationId: submitBrief
      summary: Submit a brief describing the desired customers and result.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/Brief" }
      responses:
        "200":
          description: Brief accepted (with any missing fields returned as structured needs).
          content:
            application/json:
              schema:
                type: object
                properties:
                  brief_id: { type: string, example: "brief_3Nb7" }
                  product: { type: string, example: "meetings" }
                  needs:
                    type: array
                    items: { $ref: "#/components/schemas/Need" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "422": { $ref: "#/components/responses/Invalid" }
  /quote:
    post:
      tags: [commerce]
      operationId: getQuote
      summary: Price a submitted brief.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [brief_id]
              properties:
                brief_id: { type: string, example: "brief_3Nb7" }
      responses:
        "200":
          description: A machine-readable quote.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Quote" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
  /checkout:
    post:
      tags: [commerce]
      operationId: checkout
      summary: Authorize funding (credits | x402 | stripe_acp) and open an engagement.
      description: >
        Default rail is "credits" (agent-native — no token or human; new accounts carry $25
        free starter credits). "x402" returns HTTP 402 with payment instructions until an
        X-PAYMENT settlement proof is supplied. "stripe_acp" takes a delegated shared
        payment token. On every rail,
        charges fire only as results pass acceptance, never exceed max_charge_usd, are
        itemized per result, idempotent, and reversible.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/Checkout" }
      responses:
        "200":
          description: Engagement opened.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Engagement" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402":
          description: Payment required or budget cap invalid.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
  /engagements/{engagement_id}:
    get:
      tags: [delivery]
      operationId: getEngagement
      summary: Get engagement status, spend, and results-so-far.
      parameters:
        - name: engagement_id
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Engagement status.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Engagement" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
  /engagements/{engagement_id}/results:
    get:
      tags: [delivery]
      operationId: getResults
      summary: Collect verified deliverables, each with its charge.
      parameters:
        - name: engagement_id
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Verified results.
          content:
            application/json:
              schema:
                type: object
                properties:
                  engagement_id: { type: string }
                  results:
                    type: array
                    items: { $ref: "#/components/schemas/Result" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
  /engagements/{engagement_id}/results.csv:
    get:
      tags: [delivery]
      operationId: getResultsCsv
      summary: Export delivered results as CSV.
      parameters:
        - { name: engagement_id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: CSV file (result_id, product, name, title, company, url, amount_usd, charge_status).
          content:
            text/csv: { schema: { type: string } }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
  /engagements/{engagement_id}/activity:
    get:
      tags: [delivery]
      operationId: getActivity
      summary: Live feed of what the fleet is doing for this engagement. Poll with ?since to stream.
      parameters:
        - { name: engagement_id, in: path, required: true, schema: { type: string } }
        - { name: since, in: query, required: false, schema: { type: integer }, description: Return activity with id greater than this (cursor). }
      responses:
        "200":
          description: Engagement status plus new activity lines.
          content:
            application/json:
              schema:
                type: object
                properties:
                  engagement_id: { type: string }
                  status: { type: string }
                  results_delivered: { type: integer }
                  target_volume: { type: integer }
                  spend_usd: { type: number }
                  budget_cap_usd: { type: number }
                  activity:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: integer }
                        icon: { type: string, example: "✓" }
                        message: { type: string, example: "Meeting booked & held: VP Eng @ Nimbus — billed $250" }
                        created_at: { type: string, format: date-time }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
  /activity:
    get:
      tags: [discovery]
      operationId: getGlobalActivity
      summary: Global live feed across engagements ("watch the fleet work"). Lightly anonymized.
      security: []
      parameters:
        - { name: since, in: query, required: false, schema: { type: integer } }
      responses:
        "200":
          description: Recent activity across engagements.
          content:
            application/json:
              schema:
                type: object
                properties:
                  activity:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: integer }
                        engagement: { type: string }
                        icon: { type: string }
                        message: { type: string }
                        created_at: { type: string, format: date-time }
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: fwd_live_* / fwd_test_*
  responses:
    Unauthorized:
      description: Missing or invalid API key.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    NotFound:
      description: Resource not found.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Invalid:
      description: Invalid or incomplete request.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
  schemas:
    Catalog:
      type: object
      properties:
        name: { type: string }
        pricing_model: { type: string, example: "pay_per_result" }
        products:
          type: array
          items: { $ref: "#/components/schemas/Product" }
    Product:
      type: object
      properties:
        id: { type: string, example: "meetings" }
        name: { type: string, example: "Forward Meetings" }
        result_unit: { type: string, example: "held_meeting" }
        summary: { type: string }
        acceptance:
          type: array
          items: { type: string }
    Brief:
      type: object
      required: [product, icp, volume, budget_cap_usd]
      properties:
        product:
          type: string
          enum: [leads, meetings, content, campaigns]
        difficulty:
          type: string
          enum: [standard, hard, elite]
          default: standard
          description: Scales the unit price (standard ×1.0 → elite ×1.9–2.4).
        icp:
          type: object
          properties:
            roles: { type: array, items: { type: string } }
            company_size: { type: string, example: "200-2000" }
            industry: { type: string, example: "B2B SaaS" }
            geo: { type: array, items: { type: string } }
        volume: { type: integer, minimum: 1, example: 50 }
        budget_cap_usd: { type: number, minimum: 1, example: 12000 }
        constraints:
          type: object
          additionalProperties: true
    Need:
      type: object
      properties:
        field: { type: string, example: "icp.roles" }
        message: { type: string, example: "Specify at least one target role." }
        required: { type: boolean }
    Quote:
      type: object
      properties:
        quote_id: { type: string, example: "qt_71Ce" }
        product: { type: string, example: "meetings" }
        difficulty: { type: string, example: "hard" }
        unit_price_usd: { type: number, example: 304 }
        currency: { type: string, example: "usd" }
        target_volume: { type: integer, example: 50 }
        est_total_usd: { type: number, example: 15200 }
        budget_cap_usd: { type: number, example: 15000 }
        eta_days: { type: string, example: "14-30" }
        savings_pct: { type: integer, example: 57 }
        acceptance_criteria_ref: { type: string, example: "/pricing.json#/products/meetings" }
        expires_at: { type: string, format: date-time }
    Checkout:
      type: object
      required: [quote_id]
      properties:
        quote_id: { type: string }
        payment:
          type: object
          properties:
            rail: { type: string, enum: [credits, x402, stripe_acp], default: credits }
            shared_payment_token: { type: string, example: "spt_demo", description: required for stripe_acp }
            max_charge_usd: { type: number, example: 12000 }
        webhook_url: { type: string, description: POSTs result.delivered events; Slack URLs get formatted messages. }
        billing_cadence:
          type: string
          enum: [per_result, daily, weekly, biweekly, monthly]
          default: per_result
          description: How verified charges are settled/invoiced. Charges only ever accrue for verified results.
    Engagement:
      type: object
      properties:
        engagement_id: { type: string, example: "eng_5Tq9" }
        status:
          type: string
          enum: [planning, executing, delivering, complete]
        product: { type: string }
        mode: { type: string, example: "test" }
        spend_usd: { type: number, example: 1824 }
        budget_cap_usd: { type: number, example: 15000 }
        budget_remaining_usd: { type: number, example: 13176 }
        unit_price_usd: { type: number, example: 304 }
        results_delivered: { type: integer, example: 6 }
        results_rejected: { type: integer, example: 2 }
        target_volume: { type: integer, example: 50 }
        funding_rail: { type: string, enum: [credits, stripe_acp], example: "credits" }
        billing_cadence: { type: string, enum: [per_result, daily, weekly, biweekly, monthly], example: "weekly" }
    Result:
      type: object
      properties:
        result_id: { type: string, example: "res_a1B2" }
        engagement_id: { type: string }
        product: { type: string, example: "meetings" }
        payload:
          type: object
          additionalProperties: true
        provenance:
          type: object
          additionalProperties: true
        verification:
          type: object
          properties:
            pass: { type: boolean, example: true }
            evidence:
              type: object
              additionalProperties: true
        charge: { $ref: "#/components/schemas/Charge" }
    Charge:
      type: object
      properties:
        charge_id: { type: string, example: "ch_9Lp0" }
        result_id: { type: string }
        amount_usd: { type: number, example: 240 }
        status:
          type: string
          enum: [authorized, captured, reversed]
        idempotency_key: { type: string }
        receipt_url: { type: string, example: "/api/v1/receipts/ch_9Lp0" }
    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            code: { type: string, example: "budget_cap_exceeded" }
            message: { type: string }
            field: { type: string }
