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

# Submit a conversion event

> Send a consented purchase through Plainrouter's Conversion API, preserve its event ID on retries, and inspect the event trace for Meta acceptance.

Submit a server-side conversion to Plainrouter with a Signals workspace secret. A new event returns `202`; a retry of the same event ID returns `200` with `duplicate: true`. Neither receipt alone confirms Meta delivery.

## Before sending a purchase

* Use the [Signals workspace secret](/docs/api/authentication#how-do-i-authenticate-the-rest-api) on your server. MCP execution tokens and OAuth management credentials cannot authenticate this endpoint.
* [Connect the intended Meta dataset](/docs/signals/connect-meta) for eligible destination delivery.
* Read the actual paid order and its recorded advertising consent. The example assumes all three grants; setting these fields does not obtain consent.
* Choose one purchase sender. Do not also send the same checkout through a browser purchase or Stripe settlement flow.

This endpoint uses Plainrouter's request shape, not Meta's Graph API `data` envelope. For synthetic payload checks without production writes, use the [sandbox](/docs/api/sandbox).

## Example: an immediate, consented purchase

Replace the order ID, source URL, amount and currency with the actual purchase. This immediate-event example omits `event_time`, so Plainrouter uses receipt time. For queued events, supply the original Unix timestamp or ISO-8601 payment time from your order record and preserve it on retries. Do not replace an old payment time with the current time.

<RequestExample>
  ```bash Consented purchase theme={null}
  : "${PLAINROUTER_SIGNAL_SECRET:?Set your Signals workspace secret in the server environment}"

  curl --include https://plainrouter.com/api/v1/events \
    --request POST \
    --header "Authorization: Bearer $PLAINROUTER_SIGNAL_SECRET" \
    --header "Content-Type: application/json" \
    --data '{
      "event_id": "order-123",
      "event_name": "Purchase",
      "event_source": "https://shop.example/thank-you",
      "action_source": "website",
      "consent_basis": "consent",
      "consent": {
        "ad_storage": "granted",
        "ad_user_data": "granted",
        "ad_personalization": "granted",
        "source": "checkout-consent"
      },
      "value_data": {
        "value": "49.90",
        "currency": "EUR",
        "order_id": "order-123"
      }
    }'
  ```
</RequestExample>

Use a stable, non-personal `event_id` for the logical purchase. You can use `Idempotency-Key` instead; if you supply both, they must match. Reuse the original payload after an uncertain timeout, subject to continued consent eligibility. A duplicate receipt does not update the accepted event or replay a failed Meta delivery.

<ResponseExample>
  ```json 202 — New event theme={null}
  {
    "event_id": "order-123",
    "duplicate": false,
    "warnings": []
  }
  ```

  ```json 200 — Duplicate event theme={null}
  {
    "event_id": "order-123",
    "duplicate": true
  }
  ```
</ResponseExample>

New-event acknowledgements include `warnings`. If you supply identity data without a valid consent capture time, read the [consent warning guidance](/docs/api/conversions#why-is-the-consent-capture-time-invalid) before treating the event as linked. Duplicate acknowledgements retain the response shape shown above.

## How do I verify the result?

Follow the returned `Location` header or [retrieve the event and delivery trace](https://plainrouter.com/docs/api/events/get). A delivery with `status: "accepted"` confirms Meta acceptance. Queued or retrying delivery remains pending, and an empty delivery list is not acceptance. See [delivery statuses](/docs/reference/statuses-and-terms#destination-delivery) for failed and skipped outcomes. Acceptance does not establish ad attribution or better performance.

| Failure                   | Next step                                                                                                   |
| ------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `401`                     | Stop retries and check the Signals secret and workspace.                                                    |
| `422`                     | Read field errors; check consent, decimal-string values, original event time and matching idempotency keys. |
| Timeout without a receipt | The write may have succeeded. Retry the original ID and payload rather than creating a new ID.              |

For a complete backend integration, use the [Node.js and TypeScript recipe](/docs/sdk/recipes/meta-conversions), [Python recipe](/docs/sdk/python#send-a-meta-conversion), or [Ruby recipe](/docs/sdk/ruby#send-a-meta-conversion). The parameter and response schemas below come from the [signed OpenAPI contract](https://plainrouter.com/openapi.json).


## OpenAPI

````yaml POST /events
openapi: 3.1.0
info:
  title: Plainrouter Conversion API
  version: 0.5.0
  description: Plainrouter public Signals Conversion API.
servers:
  - url: https://plainrouter.com/api/v1
    description: Production
security:
  - workspaceSecret: []
paths:
  /events:
    post:
      tags:
        - Event
      summary: Submit a conversion event
      description: >-
        Accepts a consent-aware server-side conversion event and uses event_id
        or Idempotency-Key to make retries idempotent.
      operationId: createEvent
      parameters:
        - name: Idempotency-Key
          in: header
          description: >-
            Optional idempotency key. When event_id is omitted, Plainrouter uses
            this value as event_id. If both are supplied, they must match.
          schema:
            type: string
      requestBody:
        description: >-
          Authenticated secret-key server callers may supply client_ip_address
          and client_user_agent in user_data; publishable-key browser traffic
          takes those fields only from the trusted edge.
        required: true
        content:
          application/json:
            schema:
              anyOf:
                - type: object
                  properties:
                    event_id:
                      type: string
                      description: Caller-supplied idempotency key; maximum 128 characters.
                    event_name:
                      type: string
                      description: Signal event name; maximum 100 characters.
                    parent_event_id:
                      type: string
                      description: Optional parent event id; maximum 128 characters.
                    event_time:
                      description: >-
                        Unix timestamp or ISO-8601 date-time. Defaults to
                        receipt time.
                      anyOf:
                        - type: integer
                        - type: string
                    event_source:
                      type: string
                      format: uri
                      description: Optional absolute source URL.
                    action_source:
                      type: string
                      description: >-
                        Optional action source; defaults to website and is
                        limited to 50 characters.
                    visitor_id:
                      type: string
                      description: Optional visitor identifier; maximum 255 characters.
                    consent_basis:
                      type: string
                      description: >-
                        Legal basis for processing. Legitimate-interest revenue
                        lifecycle events are rejected; use an authenticated
                        server adapter.
                      const: consent
                    consent:
                      type: object
                      description: Consent state supplied with the event.
                      additionalProperties: {}
                    consent_mode:
                      type: object
                      description: Consent Mode v2 signal values supplied with the event.
                      additionalProperties: {}
                    tcf:
                      type: object
                      description: TCF v2 data containing string and optional captured_at.
                      additionalProperties: {}
                    user_data:
                      type: object
                      description: Identity fields accepted by the tracker.
                      additionalProperties: {}
                    click_ids:
                      type: object
                      description: Advertising click identifiers.
                      additionalProperties: {}
                    value_data:
                      type: object
                      description: >-
                        Optional commerce data. Contents accepts at most 50
                        items and 16 KB serialized.
                      properties:
                        value:
                          type: string
                        currency:
                          type: string
                        order_id:
                          type: string
                        contents:
                          type: array
                          items: {}
                        num_items:
                          type: integer
                  required:
                    - event_name
                    - consent_basis
                - type: object
                  properties:
                    event_id:
                      type: string
                      description: Caller-supplied idempotency key; maximum 128 characters.
                    event_name:
                      type: string
                      description: Signal event name; maximum 100 characters.
                      enum:
                        - signal_verification
                        - consent_withdrawal
                    parent_event_id:
                      type: string
                      description: Optional parent event id; maximum 128 characters.
                    event_time:
                      description: >-
                        Unix timestamp or ISO-8601 date-time. Defaults to
                        receipt time.
                      anyOf:
                        - type: integer
                        - type: string
                    event_source:
                      type: string
                      format: uri
                      description: Optional absolute source URL.
                    action_source:
                      type: string
                      description: >-
                        Optional action source; defaults to website and is
                        limited to 50 characters.
                    visitor_id:
                      type: string
                      description: Optional visitor identifier; maximum 255 characters.
                    consent_basis:
                      type: string
                      description: >-
                        Legal basis for processing. Legitimate-interest revenue
                        lifecycle events are rejected; use an authenticated
                        server adapter.
                      const: legitimate_interest
                    consent:
                      type: object
                      description: Consent state supplied with the event.
                      additionalProperties: {}
                    consent_mode:
                      type: object
                      description: Consent Mode v2 signal values supplied with the event.
                      additionalProperties: {}
                    tcf:
                      type: object
                      description: TCF v2 data containing string and optional captured_at.
                      additionalProperties: {}
                    user_data:
                      type: object
                      description: Identity fields accepted by the tracker.
                      additionalProperties: {}
                    click_ids:
                      type: object
                      description: Advertising click identifiers.
                      additionalProperties: {}
                    value_data:
                      type: object
                      description: >-
                        Optional commerce data. Contents accepts at most 50
                        items and 16 KB serialized.
                      properties:
                        value:
                          type: string
                        currency:
                          type: string
                        order_id:
                          type: string
                        contents:
                          type: array
                          items: {}
                        num_items:
                          type: integer
                  required:
                    - event_name
                    - consent_basis
      responses:
        '200':
          description: Duplicate event accepted idempotently.
          content:
            application/json:
              schema:
                type: object
                properties:
                  event_id:
                    type: string
                  duplicate:
                    type: boolean
                required:
                  - event_id
                  - duplicate
          headers:
            Location:
              description: Canonical event resource to poll for delivery state.
              schema:
                type: string
                format: uri
            Idempotency-Key:
              description: Echoes the Idempotency-Key supplied by the caller.
              schema:
                type: string
        '202':
          description: New event accepted for processing.
          content:
            application/json:
              schema:
                type: object
                properties:
                  event_id:
                    type: string
                  duplicate:
                    type: boolean
                  warnings:
                    type: array
                    items:
                      type: object
                      properties:
                        code:
                          $ref: '#/components/schemas/IngestionWarningCode'
                        field:
                          type: string
                          const: consent.captured_at
                        message:
                          type: string
                      required:
                        - code
                        - field
                        - message
                required:
                  - event_id
                  - duplicate
                  - warnings
          headers:
            Location:
              description: Canonical event resource to poll for delivery state.
              schema:
                type: string
                format: uri
            Idempotency-Key:
              description: Echoes the Idempotency-Key supplied by the caller.
              schema:
                type: string
        '401':
          description: Invalid or missing Signals workspace secret.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorMessage'
        '422':
          description: Request validation failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationError'
components:
  schemas:
    IngestionWarningCode:
      type: string
      description: >
        The closed set of non-rejection warnings returned by authenticated
        ingestion.
      enum:
        - consent_captured_at_invalid
      title: IngestionWarningCode
    ErrorMessage:
      type: object
      properties:
        message:
          type: string
      required:
        - message
      title: ErrorMessage
    ValidationError:
      type: object
      properties:
        message:
          type: string
        errors:
          type: object
          additionalProperties:
            type: array
            items:
              type: string
      required:
        - message
        - errors
      title: ValidationError
  securitySchemes:
    workspaceSecret:
      type: http
      description: >-
        Per-workspace bearer secret. It authenticates only the Signals workspace
        represented by the credential.
      scheme: bearer

````