# Create a conversion goal

Source: https://kirora.io/docs/api/reference/createConversionGoal

`POST /v1/conversion-goals`

Requires an API key with the `conversion-goals:write` scope. [About keys and scopes](https://kirora.io/docs/api/reference/authentication)

Works with integration, agent and partner keys.

A repeat after success is refused with 409 `ALREADY_EXISTS`: read the one the first created. [About idempotency](https://kirora.io/docs/api/reference/idempotency)

Counts an event as a conversion when the contact was sent the campaign or journey within the attribution window before it. Narrow which events count with `where` (property conditions, all must hold) and `occurrence` (`first` or `repeat` of the contact's events of that name), as a workspace goal's event step does. Set `valueProperty` to sum an event property (such as `total`) as revenue, and `currency` to label it; an event whose own `currency` property names another currency converts without a value, since amounts are never converted, and so does one whose value is negative, not numeric, or too large. Events from the goal's creation on are matched as they are processed; the retained history before it is matched in the background from `countedFrom`, so the goal's results fill in shortly after it is created. A campaign or journey can count one event under several goals with different filters; a second goal with the same event and filter is refused with `409 ALREADY_EXISTS`, since it would count every conversion twice. Goals count the workspace's own events, so the Free plan is refused with 403 `PLAN_FEATURE_UNAVAILABLE`.

## Header parameters

- `Kirora-Workspace` (string, optional, pattern `^[A-Za-z0-9._:-]{1,128}$`) — Partner keys only: the child workspace to act in, by the external id the partner gave it. Without it, a partner key acts in its own workspace. Any other key that sends it is refused with 403, and an unknown workspace is 404.
- `Idempotency-Key` (string, optional, 8–255 characters) — Makes a retry safe: a repeat with the same key and body within 24 hours gets the first response again, with `Idempotency-Replay: true`. The same key with another request is 409 `IDEMPOTENCY_KEY_REUSED`; while the first is still running, 409 `IDEMPOTENCY_KEY_IN_USE`; after an `unsafe` first request failed or never returned, 409 `IDEMPOTENCY_KEY_OUTCOME_UNKNOWN`. Use a new key, such as a UUID, for each new request.

## Request body

- `campaignId` (string, optional, uuid) — The campaign it measures.
- `journeyId` (string, optional, uuid) — The journey it measures.
- `eventName` (string, required, 1–128 characters) — The event that counts as a conversion.
- `where` (object[], optional, at least 1 item, at most 5 items) — Property conditions the event must meet, up to 5; with several names, each of them must meet them.
  - `property` (string, required, 1–64 characters) — The event property key.
  - `operator` (string, required, one of `equals`, `notEquals`, `gt`, `gte`, `lt`, `lte`) — `notEquals` also matches an event without the property. `equals` and `notEquals` compare the property as text, ignoring case and accents.
  - `value` (string, required, up to 255 characters) — What to compare with, as text (`"pro"`, `"true"`); a number (`"50"`) for `gt`, `gte`, `lt` and `lte`.
- `occurrence` (string, optional, one of `first`, `repeat`) — `first`: only the contact's first event of these names; `repeat`: any after it. Read from the event history, so a contact whose earlier ones predate it counts as first seen (`coverageFrom` on progress says where the history starts); a property the customer sends (`first_order`) is the stricter signal. Absent: every one.
- `attributionWindow` (DurationInput, optional) — How long after a message the event still counts: 1 to 3,650 whole days, default 30 days.
  - `value` (integer, required, min > 0)
  - `unit` (string, required, one of `seconds`, `minutes`, `hours`, `days`)
- `valueProperty` (string, optional, 1–64 characters) — The event property summed as revenue, such as `total`.
- `currency` (string, optional, pattern `^[A-Z]{3}$`) — The revenue's currency, an ISO 4217 code; needs `valueProperty`.

## Returns

Returns `201 Created` with a `ConversionGoal` in `data`. It can also return `409 Conflict`, `413 Payload Too Large` or `415 Unsupported Media Type`; see the statuses below. Otherwise returns an error; [Errors](https://kirora.io/docs/api/reference/errors) lists every code and which to retry.

- `id` (string, required, uuid)
- `campaignId` (string | null, required, uuid) — The campaign it measures.
- `journeyId` (string | null, required, uuid) — The journey it measures.
- `eventName` (string, required) — The event that counts as a conversion.
- `where` (object[] | null, required, at least 1 item, at most 5 items) — Property conditions the event must meet; `null` counts every one.
  - `property` (string, required, 1–64 characters) — The event property key.
  - `operator` (string, required, one of `equals`, `notEquals`, `gt`, `gte`, `lt`, `lte`) — `notEquals` also matches an event without the property. `equals` and `notEquals` compare the property as text, ignoring case and accents.
  - `value` (string, required, up to 255 characters) — What to compare with, as text (`"pro"`, `"true"`); a number (`"50"`) for `gt`, `gte`, `lt` and `lte`.
- `occurrence` (string | null, required, one of `first`, `repeat`) — `first`: only the contact's first event of this name; `repeat`: any after it; `null`: every one.
- `attributionWindow` (Duration, required) — How long after a message the event still counts, in days.
  - `value` (integer, required, min > 0)
  - `unit` (string, required, one of `seconds`, `minutes`, `hours`, `days`)
- `windowChangedAt` (string | null, required, date-time) — When the attribution window was last changed; `null` if never. A change applies to events processed after it: conversions counted before keep the window they were counted under.
- `valueProperty` (string | null, required) — The event property summed as revenue; `null` counts no value.
- `currency` (string | null, required) — The revenue's currency, if named.
- `countedFrom` (string, required, date-time) — The earliest events the goal can count: when it was created, the start of the event history then retained. Events from then on are matched to the sends before them, those tracked before the goal existed included; older ones are gone.
- `createdAt` (string, required, date-time)

Statuses:

- `201 Created`: The new goal.
- `409 Conflict`: The request conflicts with current state — a duplicate unique value, or a resource whose status does not allow this operation.
- `413 Payload Too Large`: The body is larger than this operation accepts (`PAYLOAD_TOO_LARGE`).
- `415 Unsupported Media Type`: The body is not JSON, or not the upload the operation takes (`UNSUPPORTED_MEDIA_TYPE`).

Headers on `201 Created`:

- `Idempotency-Replay` (string, optional, one of `true`) — `true` when this is the stored response to an earlier request with the same key.

## Example: Count orders after the journey's messages as conversions, with revenue

```bash
curl -X POST https://api.kirora.io/v1/conversion-goals \
  -H "Authorization: Bearer $KIRORA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "journeyId": "0192f3a4-3d5e-7a7f-8c43-6b8c9d0e1f2a",
    "eventName": "Order Completed",
    "valueProperty": "total",
    "currency": "USD"
  }'
```

Response `201`:

```json
{
  "data": {
    "id": "0192f3d0-7a1b-7c2d-8e3f-4a5b6c7d8e9f",
    "campaignId": null,
    "journeyId": "0192f3a4-3d5e-7a7f-8c43-6b8c9d0e1f2a",
    "eventName": "Order Completed",
    "where": null,
    "occurrence": null,
    "attributionWindow": {
      "value": 30,
      "unit": "days"
    },
    "windowChangedAt": null,
    "valueProperty": "total",
    "currency": "USD",
    "countedFrom": "2026-07-03T09:30:00.000Z",
    "createdAt": "2026-10-01T09:30:00.000Z"
  }
}
```

## Example: Count only contacts' first orders of $50 or more

```bash
curl -X POST https://api.kirora.io/v1/conversion-goals \
  -H "Authorization: Bearer $KIRORA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "journeyId": "0192f3a4-3d5e-7a7f-8c43-6b8c9d0e1f2a",
    "eventName": "Order Completed",
    "where": [
      {
        "property": "total",
        "operator": "gte",
        "value": "50"
      }
    ],
    "occurrence": "first",
    "valueProperty": "total",
    "currency": "USD"
  }'
```

Response `201`:

```json
{
  "data": {
    "id": "0192f3d0-7a1b-7c2d-8e3f-4a5b6c7d8ea0",
    "campaignId": null,
    "journeyId": "0192f3a4-3d5e-7a7f-8c43-6b8c9d0e1f2a",
    "eventName": "Order Completed",
    "where": [
      {
        "property": "total",
        "operator": "gte",
        "value": "50"
      }
    ],
    "occurrence": "first",
    "attributionWindow": {
      "value": 30,
      "unit": "days"
    },
    "windowChangedAt": null,
    "valueProperty": "total",
    "currency": "USD",
    "countedFrom": "2026-07-03T09:30:00.000Z",
    "createdAt": "2026-10-01T09:30:00.000Z"
  }
}
```
