> ## Documentation Index
> Fetch the complete documentation index at: https://developers.introw.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Record an affiliate conversion

> Records an affiliate conversion and creates the campaign form submission it maps to.

**Attribution flow.** A partner shares an Introw affiliate link (`/r/{code}`). Introw logs a server-side click and redirects the visitor to your site with an opaque `irw_id` param. The `affiliate.js` snippet stores that id in a first-party, last-click `_introw_aff` cookie (90-day window). When the visitor converts, you send the cookie value as `clickId`. Because every conversion is anchored to a real server-side click and deduplicated, attribution can never be forged or inflated.

**Two authentication paths:**
- **Server-to-server** (recommended for backend conversions): send an `x-api-key` secret that holds the `affiliate:write` scope. No publishable key or Origin is required.
- **Browser** (via `affiliate.js`): send the campaign publishable key in the `x-introw-publishable-key` header (or the `publishableKey` body field). The request `Origin` must match the campaign's allowed-origins allowlist; an empty allowlist permits any origin.

**Responses are deliberately forgiving** so the browser snippet never surfaces errors on your page: an invalid, expired, or already-converted click returns `200` with `status: ignored`/`duplicate` rather than an error. A newly attributed conversion returns `201` with `status: recorded`.



## OpenAPI

````yaml /openapi.json post /api/v1/affiliate/conversions
openapi: 3.1.0
info:
  title: Introw API
  version: '2026-05-20'
  description: >-
    Customer-facing API for Introw. Manage partners, delegate commission payout
    operations to your finance software, embed the partner portal in your
    product with pre-authenticated sessions, and ingest affiliate conversions.
servers:
  - url: https://api.introw.io
  - url: https://api.staging.introw.io
security:
  - ApiKeyAuth: []
tags:
  - name: Partners
    description: >-
      Read and manage partners for the organisation associated with the
      authenticated API key.
  - name: Payouts
    description: >-
      Read and update commission payouts. Delegate approval, scheduling, and
      payment status to your ERP or accounts-payable stack.
  - name: Commission lines
    description: >-
      Create, read, update, decline, and detach commission line items. Lines can
      live in the pending pool or be attached to a payout.
  - name: Auth
    description: >-
      Create pre-authenticated partner portal sessions. Exchange a visitor email
      for a short-lived, single-use portal URL that you embed in an iframe
      inside your own product — one login, fully branded, no login screen for
      the partner.
  - name: Affiliate
    description: >-
      Ingest affiliate conversions from your backend or directly from the
      browser via affiliate.js. Every conversion is anchored to a signed,
      server-side click token (the first-party `_introw_aff` cookie),
      deduplicated, and mapped onto a campaign form submission — so attribution
      cannot be forged or double-counted.
paths:
  /api/v1/affiliate/conversions:
    post:
      tags:
        - Affiliate
      summary: Record an affiliate conversion
      description: >-
        Records an affiliate conversion and creates the campaign form submission
        it maps to.


        **Attribution flow.** A partner shares an Introw affiliate link
        (`/r/{code}`). Introw logs a server-side click and redirects the visitor
        to your site with an opaque `irw_id` param. The `affiliate.js` snippet
        stores that id in a first-party, last-click `_introw_aff` cookie (90-day
        window). When the visitor converts, you send the cookie value as
        `clickId`. Because every conversion is anchored to a real server-side
        click and deduplicated, attribution can never be forged or inflated.


        **Two authentication paths:**

        - **Server-to-server** (recommended for backend conversions): send an
        `x-api-key` secret that holds the `affiliate:write` scope. No
        publishable key or Origin is required.

        - **Browser** (via `affiliate.js`): send the campaign publishable key in
        the `x-introw-publishable-key` header (or the `publishableKey` body
        field). The request `Origin` must match the campaign's allowed-origins
        allowlist; an empty allowlist permits any origin.


        **Responses are deliberately forgiving** so the browser snippet never
        surfaces errors on your page: an invalid, expired, or already-converted
        click returns `200` with `status: ignored`/`duplicate` rather than an
        error. A newly attributed conversion returns `201` with `status:
        recorded`.
      operationId: recordAffiliateConversion
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AffiliateConversionRequest'
            example:
              clickId: aff_01HVK6Y8Z8Q7J8J8J8J8J8J8J8
              email: lead@acme.example
              properties:
                plan: pro
      responses:
        '200':
          description: >-
            Conversion was a duplicate or had no valid/unexpired click token, so
            nothing new was recorded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AffiliateConversionResponse'
              example:
                status: ignored
                conversionId: null
                formSubmissionId: null
        '201':
          description: Conversion recorded successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AffiliateConversionResponse'
              example:
                status: recorded
                conversionId: conv_01HVK6Y8Z8Q7J8J8J8J8J8J8J8
                formSubmissionId: fsub_01HVK6Y8Z8Q7J8J8J8J8J8J8J8
        '401':
          description: API key/scope or publishable key is missing or invalid.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiError'
        '403':
          description: Request Origin is not in the campaign allowlist (browser path).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiError'
        '422':
          description: Request body failed validation.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiError'
        '429':
          description: Rate limit exceeded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiError'
        '500':
          description: Unexpected server error.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiError'
components:
  schemas:
    AffiliateConversionRequest:
      type: object
      properties:
        clickId:
          description: >-
            Signed affiliate click token captured at click time. In the browser
            this is the value of the first-party `_introw_aff` cookie set by
            affiliate.js (read it with `window.introw.affiliate.getClickId()`).
            A missing, malformed, expired, or forged token causes the conversion
            to be safely ignored (HTTP 200) rather than rejected.
          type: string
          minLength: 1
          maxLength: 256
        publishableKey:
          description: >-
            Campaign publishable key, used only on the browser (client-side)
            path. Safe to expose publicly. Prefer sending it via the
            `x-introw-publishable-key` header; this body field is an
            alternative. Ignored on the server-to-server path, which
            authenticates with the secret `x-api-key` header and the
            `affiliate:write` scope.
          type: string
          minLength: 1
          maxLength: 128
        email:
          description: >-
            Email of the converting customer, when known. Used to enrich the
            resulting form submission and CRM record. Send an empty string or
            omit when unavailable.
          anyOf:
            - type: string
              format: email
              pattern: >-
                ^(?!\.)(?!.*\.\.)([A-Za-z0-9_'+\-\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$
            - type: string
              const: ''
        properties:
          description: >-
            Arbitrary conversion metadata (e.g. plan, deal value, order id).
            Each key is mapped onto a campaign form field via the campaign's
            field mapping; unmapped keys are stored alongside the submission.
          type: object
          propertyNames:
            type: string
          additionalProperties:
            anyOf:
              - type: string
              - type: number
              - type: boolean
              - type: 'null'
      required:
        - clickId
      additionalProperties: false
    AffiliateConversionResponse:
      type: object
      properties:
        status:
          description: >-
            Outcome of the conversion. `recorded` (HTTP 201): a new conversion
            was attributed and a form submission was created. `duplicate` (HTTP
            200): this click was already converted, so nothing new was recorded.
            `ignored` (HTTP 200): the click token was missing, invalid, or
            expired, so no attribution was made.
          type: string
          enum:
            - recorded
            - duplicate
            - ignored
        conversionId:
          description: >-
            Affiliate conversion id, when a new conversion was recorded;
            otherwise null.
          anyOf:
            - type: string
            - type: 'null'
        formSubmissionId:
          description: >-
            Form submission id created from the conversion, when available;
            otherwise null.
          anyOf:
            - type: string
            - type: 'null'
      required:
        - status
        - conversionId
        - formSubmissionId
      additionalProperties: false
    PublicApiError:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              description: Stable machine-readable error code.
              type: string
            message:
              description: Human-readable error message.
              type: string
            details:
              description: Optional structured details for validation and debugging.
          required:
            - code
            - message
          additionalProperties: false
      required:
        - error
      additionalProperties: false
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key
      description: Introw API key shown once when the credential is created.

````