openapi: 3.1.0
info:
  title: Jambo Contact Integration API
  version: 1.0.0
  description: |
    Programmatic endpoints for connecting Jambo Contact to external systems.

    **Base URL:** `https://jambo.contact/api`

    These routes are served by the public Next.js app and proxied to the private API service.
    Authenticate with a workspace API key (`jam_…`) — create one in **Dashboard → Settings → Developers**
    (`read`/`write` scopes for the REST API) or **Settings → Integrations → Zapier** (Zapier-only keys).
    Outbound event delivery uses signed webhooks — see [Webhook events](/developers/webhooks).
  contact:
    name: Jambo Contact
    url: https://jambo.contact/contact
  license:
    name: Proprietary
    url: https://jambo.contact/service

servers:
  - url: https://jambo.contact/api
    description: Production

tags:
  - name: REST API
    description: |
      Programmatic workspace access — list and upsert contacts, list submissions.
      Requires an API key with `read` (GET) or `write` (POST) scope, created in
      **Dashboard → Settings → Developers**. Rate limit: 120 requests/minute per key.
  - name: REST Hooks
    description: Subscribe and poll workspace events (used by the Zapier app and custom integrations).
  - name: Public forms
    description: Load published form schemas and submit responses from embedded or hosted forms.

security:
  - ApiKeyAuth: []

paths:
  /v1/contacts:
    get:
      operationId: listContacts
      tags: [REST API]
      summary: List contacts
      description: |
        Returns workspace contacts, newest first. Requires the `read` scope.
        Paginate with `cursor` — pass the `nextCursor` value from the previous page until it is `null`.
      parameters:
        - $ref: '#/components/parameters/PageLimit'
        - $ref: '#/components/parameters/PageCursor'
        - name: q
          in: query
          description: Case-insensitive search across contact name and email
          schema:
            type: string
      responses:
        '200':
          description: Page of contacts
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/OkResponse'
                  - type: object
                    properties:
                      contacts:
                        type: array
                        items:
                          $ref: '#/components/schemas/Contact'
                      nextCursor:
                        type: string
                        format: uuid
                        nullable: true
                        description: Cursor for the next page, or `null` when there are no more results
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/ServerError'
    post:
      operationId: upsertContact
      tags: [REST API]
      summary: Create or update a contact
      description: |
        Upserts a contact by email (or phone when no email is given). Requires the `write` scope.
        Returns `201` with `created: true` for new contacts, `200` with `created: false` for updates.
        Creating a contact emits a `contact.created` webhook event; updating emits `contact.updated`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpsertContactRequest'
            example:
              fullName: Ada Lovelace
              primaryEmail: ada@example.com
              company: Analytical Engines Ltd
              lifecycleStage: lead
      responses:
        '200':
          description: Existing contact updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UpsertContactResponse'
        '201':
          description: Contact created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UpsertContactResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/ServerError'

  /v1/contacts/{contactId}:
    get:
      operationId: getContact
      tags: [REST API]
      summary: Get a contact
      description: Returns a single contact by id. Requires the `read` scope.
      parameters:
        - name: contactId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Contact
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/OkResponse'
                  - type: object
                    properties:
                      contact:
                        $ref: '#/components/schemas/Contact'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Contact not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/ServerError'

  /v1/submissions:
    get:
      operationId: listSubmissions
      tags: [REST API]
      summary: List form submissions
      description: |
        Returns form submissions, newest first. Requires the `read` scope.
        Paginate with `cursor`; filter by form or time window.
      parameters:
        - $ref: '#/components/parameters/PageLimit'
        - $ref: '#/components/parameters/PageCursor'
        - name: formId
          in: query
          description: Only return submissions for this form
          schema:
            type: string
            format: uuid
        - name: since
          in: query
          description: Only return submissions at or after this ISO 8601 timestamp
          schema:
            type: string
            format: date-time
      responses:
        '200':
          description: Page of submissions
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/OkResponse'
                  - type: object
                    properties:
                      submissions:
                        type: array
                        items:
                          $ref: '#/components/schemas/Submission'
                      nextCursor:
                        type: string
                        format: uuid
                        nullable: true
                        description: Cursor for the next page, or `null` when there are no more results
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/ServerError'

  /integrations/zapier/hooks:
    post:
      operationId: createZapierHook
      tags: [REST Hooks]
      summary: Subscribe to events
      description: |
        Registers an HTTPS callback URL and event filter. Jambo delivers matching events as signed
        JSON POST requests to `targetUrl` (same envelope as [outbound webhooks](/developers/webhooks)).

        Used by the official Zapier app; you can also call this directly for custom REST Hook integrations.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateHookRequest'
            examples:
              submissionOnly:
                summary: New form submissions
                value:
                  targetUrl: https://hooks.example.com/jambo/submissions
                  eventTypes:
                    - submission.created
              multiEvent:
                summary: Submissions and contacts
                value:
                  targetUrl: https://hooks.example.com/jambo/all
                  eventTypes:
                    - submission.created
                    - contact.created
                    - contact.updated
      responses:
        '201':
          description: Hook created
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/OkResponse'
                  - type: object
                    properties:
                      id:
                        type: string
                        format: uuid
                      targetUrl:
                        type: string
                        format: uri
                      eventTypes:
                        type: array
                        items:
                          $ref: '#/components/schemas/WebhookEventType'
              example:
                ok: true
                id: 550e8400-e29b-41d4-a716-446655440000
                targetUrl: https://hooks.example.com/jambo/submissions
                eventTypes:
                  - submission.created
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/ServerError'

  /integrations/zapier/hooks/{hookId}:
    delete:
      operationId: deleteZapierHook
      tags: [REST Hooks]
      summary: Unsubscribe from events
      description: Removes a REST Hook subscription. No further events are delivered to the hook URL.
      parameters:
        - $ref: '#/components/parameters/HookId'
      responses:
        '200':
          description: Hook deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OkResponse'
              example:
                ok: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Hook not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                ok: false
                error: Hook not found
        '500':
          $ref: '#/components/responses/ServerError'

  /integrations/zapier/events/sample:
    get:
      operationId: getSampleEvent
      tags: [REST Hooks]
      summary: Sample event payload
      description: |
        Returns a synthetic event envelope for the given type. Useful for Zapier field mapping and
        integration testing without waiting for live traffic.
      parameters:
        - $ref: '#/components/parameters/EventTypeQuery'
      responses:
        '200':
          description: Sample webhook envelope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/ServerError'

  /integrations/zapier/events/recent:
    get:
      operationId: listRecentEvents
      tags: [REST Hooks]
      summary: Recent live events
      description: |
        Returns the most recent recorded events for the workspace, newest first. Each item uses the
        same envelope shape as outbound webhook deliveries.
      parameters:
        - $ref: '#/components/parameters/EventTypeQuery'
        - name: limit
          in: query
          description: Number of events (1–10, default 3)
          schema:
            type: integer
            minimum: 1
            maximum: 10
            default: 3
      responses:
        '200':
          description: Recent event envelopes
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/WebhookEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/ServerError'

  /public/forms/{orgId}/{slug}:
    get:
      operationId: getPublishedForm
      tags: [Public forms]
      summary: Load a published form
      description: |
        Returns the published form schema and styling for public rendering. No authentication required.
        Returns 404 if the form is unpublished or the slug does not match.
      security: []
      parameters:
        - $ref: '#/components/parameters/OrgId'
        - $ref: '#/components/parameters/FormSlug'
      responses:
        '200':
          description: Published form payload
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublishedForm'
        '404':
          description: Published form not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /public/forms/{orgId}/{slug}/uploads/presign:
    post:
      operationId: presignFormUpload
      tags: [Public forms]
      summary: Presign a file upload
      description: |
        Returns a presigned upload URL for a file field on a published form. No authentication required.
        Upload the file bytes to the returned URL, then reference the `uploadId` in the field value when
        submitting the form. The presigned URL expires after 15 minutes.
      security: []
      parameters:
        - $ref: '#/components/parameters/OrgId'
        - $ref: '#/components/parameters/FormSlug'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [fieldKey]
              properties:
                fieldKey:
                  type: string
                  description: Key of the file upload field in the form spec
                uploadId:
                  type: string
                  description: Optional client-generated upload id; a UUID is generated when omitted
                filename:
                  type: string
                  default: upload.bin
                contentType:
                  type: string
                  default: application/octet-stream
                byteLength:
                  type: integer
                  description: File size in bytes — validated against the field's max size rule
      responses:
        '200':
          description: Presigned upload created
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/OkResponse'
                  - type: object
                    properties:
                      uploadId:
                        type: string
                      fieldKey:
                        type: string
                      filename:
                        type: string
                      contentType:
                        type: string
                      maxSizeBytes:
                        type: integer
                        nullable: true
                      acceptMimeTypes:
                        type: array
                        items:
                          type: string
                      uploadUrl:
                        type: string
                        format: uri
                        description: Presigned URL to PUT the file bytes to
                      publicUrl:
                        type: string
                        format: uri
                      key:
                        type: string
                        description: Object storage key for the upload
                      expiresInSeconds:
                        type: integer
                        const: 900
                      headers:
                        type: object
                        additionalProperties:
                          type: string
                        description: Headers to send with the PUT request (includes Content-Type)
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          description: Published form or file field not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '413':
          description: File exceeds the field's size limit
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /submissions:
    post:
      operationId: createSubmission
      tags: [Public forms]
      summary: Submit a form response
      description: |
        Accepts a form submission from hosted pages, iframe embeds, or custom front-ends.
        Requires a `formId` in the body; field values go in `payload`. CORS is enabled for browser
        submissions. Successful live submissions emit `submission.created` and `message.received`
        webhook events.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [formId]
              properties:
                formId:
                  type: string
                  format: uuid
                payload:
                  type: object
                  additionalProperties: true
                  description: |
                    Field values keyed by field key from the form spec. Common keys like `full_name`,
                    `email`, `phone`, and `message` are used to create the contact and lead.
                testMode:
                  type: boolean
                  description: When true, uses the builder preview version (not live traffic).
                turnstileToken:
                  type: string
                  description: Cloudflare Turnstile token — required when the form has bot protection enabled.
                sourceUrl:
                  type: string
                  format: uri
                  description: Page URL the submission came from (stored on the submission).
                referrerUrl:
                  type: string
                  format: uri
                userAgent:
                  type: string
                  description: Optional override; the server falls back to the User-Agent header.
                utm:
                  type: object
                  additionalProperties:
                    type: string
                  description: UTM parameters captured with the submission.
      responses:
        '200':
          description: Submission accepted
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/OkResponse'
                  - type: object
                    description: |
                      Includes the created records. Object shapes follow the internal data model;
                      treat fields beyond the ids as unstable.
                    properties:
                      submission:
                        type: object
                        properties:
                          id:
                            type: string
                            format: uuid
                          formId:
                            type: string
                            format: uuid
                          formVersionId:
                            type: string
                            format: uuid
                            nullable: true
                      contact:
                        type: object
                        additionalProperties: true
                      lead:
                        type: object
                        additionalProperties: true
                      conversation:
                        type: object
                        additionalProperties: true
                      inboundMessage:
                        type: object
                        additionalProperties: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '403':
          description: Form not published or submission blocked
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Form not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: No active published version
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

components:
  securitySchemes:
    ApiKeyAuth:
      type: http
      scheme: bearer
      description: |
        Workspace API key, prefixed with `jam_`. You may also send the key via the `X-Api-Key` header.

        Keys are scoped:

        - **`read` / `write`** — for the `/v1` REST API. Create in **Dashboard → Settings → Developers**.
        - **`zapier`** — for the REST Hooks endpoints only. Create in **Dashboard → Settings → Integrations → Zapier**.

  parameters:
    PageLimit:
      name: limit
      in: query
      description: Page size (1–100, default 50)
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 50
    PageCursor:
      name: cursor
      in: query
      description: Opaque cursor from the previous page's `nextCursor`
      schema:
        type: string
        format: uuid
    HookId:
      name: hookId
      in: path
      required: true
      schema:
        type: string
        format: uuid
    OrgId:
      name: orgId
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: Workspace (organization) ID
    FormSlug:
      name: slug
      in: path
      required: true
      schema:
        type: string
      description: URL slug of the published form
    EventTypeQuery:
      name: type
      in: query
      description: Event type filter
      schema:
        $ref: '#/components/schemas/WebhookEventType'
        default: submission.created

  schemas:
    OkResponse:
      type: object
      required: [ok]
      properties:
        ok:
          type: boolean
          const: true

    ErrorResponse:
      type: object
      required: [ok, error]
      properties:
        ok:
          type: boolean
          const: false
        error:
          type: string

    Contact:
      type: object
      required: [id]
      properties:
        id:
          type: string
          format: uuid
        fullName:
          type: string
          nullable: true
        primaryEmail:
          type: string
          nullable: true
        primaryPhone:
          type: string
          nullable: true
        jobTitle:
          type: string
          nullable: true
        company:
          type: string
          nullable: true
        websiteUrl:
          type: string
          nullable: true
        avatarUrl:
          type: string
          nullable: true
        headerColor:
          type: string
          nullable: true
        lifecycleStage:
          type: string
          nullable: true
        notes:
          type: string
          nullable: true
        submissionCount:
          type: integer
          description: Number of recent form submissions included in the lookup (max 5)
        lastSubmittedAt:
          type: string
          format: date-time
          nullable: true
        sourceForms:
          type: array
          items:
            type: string
          description: Names of forms this contact has submitted
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    UpsertContactRequest:
      type: object
      description: At least one of `fullName`, `primaryEmail`, or `primaryPhone` is required.
      properties:
        fullName:
          type: string
        primaryEmail:
          type: string
          format: email
          description: Used as the upsert match key (case-insensitive)
        primaryPhone:
          type: string
          description: Used as the upsert match key when no email is given
        jobTitle:
          type: string
        lifecycleStage:
          type: string
        notes:
          type: string
        company:
          type: string
        websiteUrl:
          type: string
          format: uri

    UpsertContactResponse:
      allOf:
        - $ref: '#/components/schemas/OkResponse'
        - type: object
          properties:
            created:
              type: boolean
              description: True when a new contact was created, false when an existing one was updated
            contact:
              $ref: '#/components/schemas/Contact'

    Submission:
      type: object
      required: [id, formId, submittedAt]
      properties:
        id:
          type: string
          format: uuid
        formId:
          type: string
          format: uuid
        form:
          type: object
          nullable: true
          properties:
            id:
              type: string
              format: uuid
            name:
              type: string
            slug:
              type: string
        contactId:
          type: string
          format: uuid
          nullable: true
        conversationId:
          type: string
          format: uuid
          nullable: true
        status:
          type: string
        payload:
          type: object
          additionalProperties: true
          description: Normalized field values keyed by field key
        sourceUrl:
          type: string
          nullable: true
        referrerUrl:
          type: string
          nullable: true
        submittedAt:
          type: string
          format: date-time

    PublishedForm:
      type: object
      description: Published form payload for public rendering.
      required: [ok, organizationId, formId, name, slug, spec]
      properties:
        ok:
          type: boolean
          const: true
        organizationId:
          type: string
          format: uuid
        formId:
          type: string
          format: uuid
        name:
          type: string
        slug:
          type: string
        status:
          type: string
          const: published
        spec:
          type: object
          additionalProperties: true
          description: Form schema — `fields` array plus `settings` and `meta` (name, slug, status).
        customCss:
          type: string
          description: Workspace-defined CSS injected into the hosted page (empty when unset)
        logoUrl:
          type: string
          description: Branding logo URL from the form settings (empty when unset)
        hostedPath:
          type: string
          description: Path of the hosted form page (`/f/{orgId}/{slug}`)
        embedPath:
          type: string
          description: Path of the embeddable form page
        publishedVersionId:
          type: string
          format: uuid
        render:
          type: object
          properties:
            htmlStrategy:
              type: string
            cssStrategy:
              type: string

    WebhookEventType:
      type: string
      enum:
        - submission.created
        - contact.created
        - contact.updated
        - message.received
        - message.sent
        - task.created
        - task.updated
        - task.deleted

    CreateHookRequest:
      type: object
      required: [targetUrl]
      properties:
        targetUrl:
          type: string
          format: uri
          description: HTTPS URL that receives signed POST deliveries
        eventTypes:
          type: array
          items:
            $ref: '#/components/schemas/WebhookEventType'
          default: [submission.created]
          description: Events to deliver to this hook. Defaults to `submission.created` when omitted.

    WebhookEnvelope:
      type: object
      required: [id, type, version, occurredAt, workspaceId, object, data]
      properties:
        id:
          type: string
          format: uuid
          description: Unique event id — use for deduplication (at-least-once delivery)
        type:
          $ref: '#/components/schemas/WebhookEventType'
        version:
          type: integer
          const: 1
        occurredAt:
          type: string
          format: date-time
        workspaceId:
          type: string
          format: uuid
        object:
          type: object
          required: [type, id]
          properties:
            type:
              type: string
              enum: [submission, contact, message, task]
            id:
              type: string
              format: uuid
              nullable: true
        data:
          type: object
          additionalProperties: true
          description: Normalized event payload — shape varies by event type

  responses:
    BadRequest:
      description: Invalid request
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Unauthorized:
      description: Missing or invalid API key
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            ok: false
            error: Invalid API key.
    Forbidden:
      description: API key is missing the required scope
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            ok: false
            error: This API key is missing the required `read` scope.
    RateLimited:
      description: Rate limit exceeded (120 requests/minute per API key)
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            ok: false
            error: Rate limit exceeded. Maximum 120 requests per minute per API key.
    ServerError:
      description: Unexpected server error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
