openapi: 3.1.0
info:
  title: GoAsk 3D Configurator API
  version: "2026-10-01"
  summary: Read your configurator leads and builds, and receive signed webhooks.
  description: |
    The public API of the GoAsk 3D Configurator, for a manufacturer's own systems (CRM, ERP, Zapier,
    an integrator's code). Two parts:

    - **Read API**: your leads and builds, with an API key from your configurator's `/developer` page.
    - **Webhooks**: signed events (Standard Webhooks) sent to your https endpoint the moment a lead,
      build, quote or won deal happens.

    Base URL: your own configurator's address, for example `https://yourbrand.3dconfigurator.com.au`.
    The API key decides whose data you get, so the same key works on any of our hosts.

    **Versioning.** This is version `2026-10-01`. We may add fields and endpoints at any time: write your
    code to ignore fields it does not know. Renaming or removing a field, or changing its meaning, only
    happens in a new version, announced in the changelog first. Webhook payloads carry `api_version`.
servers:
  - url: https://{configurator}
    variables:
      configurator:
        default: demo.3dconfigurator.com.au
        description: Your configurator's address
security:
  - apiKey: []
tags:
  - name: Leads
  - name: Builds
paths:
  /api/v1/leads:
    get:
      tags: [Leads]
      summary: List leads, oldest change first
      description: |
        Pages through your leads ordered by when they last changed (`updated_at`, then `id`), so a lead
        that changes while you page moves to the end instead of being skipped.

        **Keeping in sync.** Store the largest `updated_at` you have seen. Next time, ask for
        `updated_since` = that value **minus one minute** and de-duplicate by `id`: you may get a few leads
        twice, you never miss one. Webhooks are not guaranteed to arrive; this list is. Deleted leads are
        not reported.
      parameters:
        - name: updated_since
          in: query
          description: Only leads that changed at or after this time (ISO 8601).
          schema: { type: string, format: date-time, example: "2026-10-01T00:00:00Z" }
        - name: limit
          in: query
          description: Leads per page, 1 to 200.
          schema: { type: integer, minimum: 1, maximum: 200, default: 50 }
        - name: cursor
          in: query
          description: The `next_cursor` of the previous page.
          schema: { type: string }
      responses:
        "200":
          description: A page of leads.
          content:
            application/json:
              schema:
                type: object
                required: [data, has_more, next_cursor]
                properties:
                  data: { type: array, items: { $ref: "#/components/schemas/Lead" } }
                  has_more: { type: boolean }
                  next_cursor: { type: [string, "null"], description: Pass as `cursor` for the next page. }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /api/v1/leads/{lead_id}:
    get:
      tags: [Leads]
      summary: One lead
      parameters:
        - name: lead_id
          in: path
          required: true
          schema: { type: string, example: lead_123 }
      responses:
        "200":
          description: The lead.
          content: { application/json: { schema: { $ref: "#/components/schemas/Lead" } } }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /api/v1/builds/{build_id}:
    get:
      tags: [Builds]
      summary: One build, without the person
      description: The `build_id` is the one webhooks carry (`bld_...`).
      parameters:
        - name: build_id
          in: path
          required: true
          schema: { type: string, example: bld_9a440c74f6666053cd29b31e1c381d64 }
      responses:
        "200":
          description: The build.
          content: { application/json: { schema: { $ref: "#/components/schemas/Build" } } }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
webhooks:
  lead.created:
    post:
      summary: A buyer saved a build as a lead ("Get my build")
      requestBody:
        content: { application/json: { schema: { $ref: "#/components/schemas/LeadCreatedEvent" } } }
      responses: { "2XX": { description: Received. Answer within 10 seconds. } }
  build.created:
    post:
      summary: The same build, without the person (for analytics)
      requestBody:
        content: { application/json: { schema: { $ref: "#/components/schemas/BuildCreatedEvent" } } }
      responses: { "2XX": { description: Received. } }
  quote.sent:
    post:
      summary: The quote was emailed to the buyer
      requestBody:
        content: { application/json: { schema: { $ref: "#/components/schemas/StatusEvent" } } }
      responses: { "2XX": { description: Received. } }
  lead.won:
    post:
      summary: A dealer marked the deal won
      requestBody:
        content: { application/json: { schema: { $ref: "#/components/schemas/StatusEvent" } } }
      responses: { "2XX": { description: Received. } }
components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      description: |
        `Authorization: Bearer gak_live_...`. Make keys on your configurator's `/developer` page. A key is
        shown once; we keep only a hash. 120 requests a minute per key.
  responses:
    BadRequest:
      description: A query parameter is not valid.
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    Unauthorized:
      description: Missing, unknown or revoked API key.
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    Forbidden:
      description: The key does not have the scope for this.
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    NotFound:
      description: Not found for this key's account (another account's data always reads as missing).
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    RateLimited:
      description: Over 120 requests this minute. Wait `Retry-After` seconds.
      headers: { Retry-After: { schema: { type: integer } } }
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
  schemas:
    Error:
      type: object
      properties: { error: { type: string } }
    Money: { type: [number, "null"], description: AUD, including GST. }
    Vehicle:
      type: object
      properties:
        name: { type: [string, "null"], example: Toyota Hilux Double Cab SR5 2026 }
        colour: { type: [string, "null"], example: Crystal Pearl }
    Item:
      type: object
      properties:
        sku: { type: [string, "null"], example: G4X4-BULLBAR-4X4 }
        name: { type: [string, "null"], example: 4x4 Alloy Bull Bar }
        qty: { type: integer, example: 1 }
        unit_price: { $ref: "#/components/schemas/Money" }
    Customer:
      type: object
      description: Only with a key that has "customer details" (`read:customers`), or a webhook with customer details on.
      properties:
        name: { type: [string, "null"] }
        email: { type: [string, "null"] }
        mobile: { type: [string, "null"] }
    Lead:
      type: object
      required: [id, build_id, created_at, updated_at, status]
      properties:
        id: { type: string, example: lead_123 }
        build_id: { type: string, example: bld_9a440c74f6666053cd29b31e1c381d64 }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        status: { type: string, enum: [proposed, won] }
        dealership: { type: [string, "null"], example: Norris Group }
        vehicle: { $ref: "#/components/schemas/Vehicle" }
        items: { type: array, items: { $ref: "#/components/schemas/Item" } }
        total: { $ref: "#/components/schemas/Money" }
        weekly: { $ref: "#/components/schemas/Money" }
        currency: { type: string, example: AUD }
        marketing_consent: { type: boolean }
        customer: { $ref: "#/components/schemas/Customer" }
        rego: { type: [string, "null"], description: With customer details only. }
        salesperson: { type: [string, "null"], description: With customer details only. }
    Build:
      type: object
      properties:
        build_id: { type: string }
        vehicle: { $ref: "#/components/schemas/Vehicle" }
        items: { type: array, items: { $ref: "#/components/schemas/Item" } }
        total: { $ref: "#/components/schemas/Money" }
        currency: { type: string, example: AUD }
        created_at: { type: string, format: date-time }
    EventEnvelope:
      type: object
      required: [id, type, timestamp, api_version, tenant, data]
      description: |
        Headers: `webhook-id` (= `id`, the same on every retry: de-duplicate on it), `webhook-timestamp`
        (seconds, per attempt), `webhook-signature` (`v1,<base64>`; during a secret change two signatures,
        separated by a space).
      properties:
        id: { type: string, example: evt_d1024fc881e9becfc1c23952 }
        type: { type: string }
        timestamp: { type: string, format: date-time }
        api_version: { type: string, example: "2026-10-01" }
        tenant: { type: string, example: goask }
    LeadCreatedEvent:
      allOf:
        - $ref: "#/components/schemas/EventEnvelope"
        - type: object
          properties:
            type: { const: lead.created }
            data:
              type: object
              properties:
                lead_id: { type: string }
                build_id: { type: string }
                dealership: { type: [string, "null"] }
                vehicle: { $ref: "#/components/schemas/Vehicle" }
                items: { type: array, items: { $ref: "#/components/schemas/Item" } }
                total: { $ref: "#/components/schemas/Money" }
                weekly: { $ref: "#/components/schemas/Money" }
                currency: { type: string }
                marketing_consent: { type: boolean }
                status: { type: string }
                updated_at: { type: string, format: date-time }
                customer: { $ref: "#/components/schemas/Customer" }
                rego: { type: [string, "null"] }
                salesperson: { type: [string, "null"] }
    BuildCreatedEvent:
      allOf:
        - $ref: "#/components/schemas/EventEnvelope"
        - type: object
          properties:
            type: { const: build.created }
            data:
              type: object
              description: Never a person, never a lead id.
              properties:
                build_id: { type: string }
                vehicle: { $ref: "#/components/schemas/Vehicle" }
                items: { type: array, items: { $ref: "#/components/schemas/Item" } }
                total: { $ref: "#/components/schemas/Money" }
                currency: { type: string }
                created_at: { type: string, format: date-time }
    StatusEvent:
      allOf:
        - $ref: "#/components/schemas/EventEnvelope"
        - type: object
          properties:
            type: { enum: [quote.sent, lead.won] }
            data:
              type: object
              properties:
                lead_id: { type: string }
                build_id: { type: string }
                dealership: { type: [string, "null"] }
                status: { type: string, enum: [sent, won] }
                total: { $ref: "#/components/schemas/Money" }
                currency: { type: string }
                updated_at: { type: string, format: date-time }
                customer: { $ref: "#/components/schemas/Customer" }
