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

# Get one conversion event

> One event by its event ID.

```ts AdonisJS wrap theme={"dark"}
import app from '@adonisjs/core/services/app'
import { SiteClient } from '@fetchski/attract-cms-sdk'

const site = await app.container.make(SiteClient)
const getConversionEvent = await site.conversionEvents().getConversionEvent({ eventId: 'tenant-42:order:1234:purchase' })
```

```json 200 wrap theme={"dark"}
{
  "event": {
    "eventId": "b8f2c1de-4f5a-4c1e-9a53-0d6f2b7c1e9a",
    "conversion": {
      "id": "ckq1w2e3r4t5y6u7i8o9p0",
      "key": "quote-request",
      "title": "Quote request"
    },
    "trigger": {
      "type": "form",
      "ref": "ckformid123"
    },
    "source": "browser",
    "occurredAt": "2026-09-21T15:04:05.000Z",
    "recordedAt": "2026-09-21T15:04:05.412Z",
    "updatedAt": "2026-09-21T15:04:07.020Z",
    "page": {
      "url": "https://example.com/contact?gclid=Cj0KCQjw",
      "path": "/contact",
      "referrer": "https://www.google.com"
    },
    "visitorId": "0b8e6a4c-8d2f-4b6e-9a1c-3f5d7e9b1a2c",
    "attribution": {
      "firstTouch": {
        "at": "2026-09-20T09:12:00.000Z",
        "path": "/",
        "landingUrl": "https://example.com/?gclid=Cj0KCQjw",
        "referrerOrigin": "https://www.google.com",
        "adPlatforms": [
          "google"
        ],
        "campaign": {
          "utm_source": "google",
          "utm_campaign": "spring-sale"
        },
        "clickIds": {
          "gclid": "Cj0KCQjw"
        }
      },
      "adPlatforms": [
        "google"
      ],
      "campaign": {
        "utm_source": "google",
        "utm_campaign": "spring-sale"
      },
      "clickIds": {
        "gclid": "Cj0KCQjw"
      },
      "browserIds": {
        "_gcl_au": "1.1.1234567890.1790000000"
      },
      "provenance": {
        "gclid": "ourRecord",
        "_gcl_au": "platformCookie"
      }
    },
    "consent": {
      "categories": {
        "analytics": true,
        "marketing": true
      },
      "mode": "lenient",
      "country": "US",
      "gpc": false,
      "policyVersion": 7
    },
    "value": 50,
    "currency": "USD",
    "userData": {
      "emailSha256": "b4c9a289323b21a01c3e940f150eb9b8c542587f1abfd8f0e1cc1ffc5e475514",
      "phoneSha256": "b4c9a289323b21a01c3e940f150eb9b8c542587f1abfd8f0e1cc1ffc5e475514",
      "phoneDigitsSha256": "b4c9a289323b21a01c3e940f150eb9b8c542587f1abfd8f0e1cc1ffc5e475514"
    },
    "submissionId": "ck9submission1",
    "delivery": {
      "google-ads": {
        "browser": "sent",
        "server": {
          "status": "sent",
          "attempts": 1,
          "sentAt": "2026-09-21T15:04:07.001Z",
          "lastAttemptAt": "2026-09-21T15:04:07.001Z"
        }
      },
      "ga4": {
        "browser": "sent",
        "server": {
          "status": "n/a",
          "attempts": 0
        }
      }
    },
    "status": "active",
    "adjustments": []
  }
}
```

## In your app

A controller answers a route on your app: it calls `getConversionEvent()` and returns the answer.

**1. Add the controller**

```ts app/controllers/get_conversion_event_controller.ts theme={"dark"}
import { inject } from '@adonisjs/core'
import type { HttpContext } from '@adonisjs/core/http'
import { SiteClient } from '@fetchski/attract-cms-sdk'

@inject()
export default class GetConversionEventController {
  constructor(protected site: SiteClient) {}

  async handle({ params }: HttpContext) {
    return this.site.conversionEvents().getConversionEvent({ eventId: params.eventId })
  }
}
```

**2. Add the route**

```ts start/routes.ts theme={"dark"}
import router from '@adonisjs/core/services/router'

const GetConversionEventController = () => import('#controllers/get_conversion_event_controller')

router.get('/conversion-event/:eventId', [GetConversionEventController])
```

**3. Call it**

```bash Terminal theme={"dark"}
curl http://localhost:3333/conversion-event/tenant-42:order:1234:purchase
```

With `node ace serve` running, it prints its answer as JSON.

> [accordion] Required parameters
>
> One object, e.g. `getConversionEvent({ eventId })`; leave out the optional fields.
>
> **eventId** `string` *required*
>
> **Required.** The event ID. Matches `^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$`. At most 128 characters.

> [accordion] Returns
>
> A `GetConversionEventResponse`. The event. Read its properties (`getConversionEvent.event`): each says whether the site always sends it.
>
> **event** `ConversionEvent`
>
> **Always sent.** A `ConversionEvent`. One conversion occurrence. Fields the visitor’s consent did not allow are ABSENT, not empty. Never returned: the visitor’s IP address and user agent, outbox lease and retry bookkeeping.
>
> **ConversionEvent**
>
> **eventId** `string`
>
> **Always sent.** Opaque, unique per site, stable. The CMS mints a UUID for events it originates; another system mints its own (e.g. `tenant-42:order:1234:purchase`). Forwarded unchanged to every ad platform as its de-duplication key. Matches `^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$`. At most 128 characters.
>
> **conversion** `ConversionEventConversion`
>
> **Always sent.** A `ConversionEventConversion`. The Conversion it counts for.
>
> **ConversionEventConversion**
>
> **id** `string`
>
> **Always sent.** The Conversion’s id.
>
> **key** `string`
>
> **Always sent.** The Conversion’s key when the event was recorded.
>
> **title** `string`
>
> **Sometimes sent:** `undefined` when the site leaves it out. The Conversion’s name in the CMS.
>
> **source** `ConversionEventSource`
>
> **Always sent.** How it was recorded: `browser` (the site’s tag), `external` (posted through the Conversion Data API), or `server` (the site’s own server; nothing records one yet). One of `browser`, `server`, `external`; a newer site may send others, so keep a fallback.
>
> **occurredAt** `Date`
>
> **Always sent.** When it happened. Format: an ISO 8601 date and time.
>
> **updatedAt** `Date`
>
> **Always sent.** Last change (delivery outcome, adjustment) — the sync order. Format: an ISO 8601 date and time.
>
> **delivery** `Readonly\<Record\<string, DeliveryState\>\>`
>
> **Always sent.** A map of names to `DeliveryState`. Per destination the Conversion reports to.
>
> **DeliveryState**
>
> **browser** `Browser`
>
> **Sometimes sent:** `undefined` when the site leaves it out. Whether the page fired it at this platform’s tag; `n/a` when the Conversion sends to this platform from the server only. One of `sent`, `not-sent`, `n/a`; a newer site may send others, so keep a fallback.
>
> **server** `ServerDeliveryState`
>
> **Sometimes sent:** `undefined` when the site leaves it out. A `ServerDeliveryState`. Server-side delivery to this platform’s API (Google Data Manager, GA4 Measurement Protocol, Meta Conversions API, TikTok Events API).
>
> **ServerDeliveryState**
>
> **status** `ServerDeliveryStateStatus`
>
> **Always sent.** `queued`/`retry` — owed; `sent` — delivered (`test: true` in a platform test mode); `failed` — attempts used up or a permanent error; `not-configured` — credentials, pixel, Google tag or conversion action missing; `expired` — older than the platform accepts (Google Ads 90 days, Meta and TikTok 7, Google Analytics 3); `not-sent` — retracted first, or nothing the platform can match; `no-consent` — the visitor did not allow Marketing; `n/a` — the platform has no server API here. One of `queued`, `retry`, `sent`, `failed`, `not-configured`, `expired`, `not-sent`, `no-consent`, `n/a`; a newer site may send others, so keep a fallback.
>
> **attempts** `number`
>
> **Always sent.** Sends tried so far. At least 0.
>
> **sentAt** `Date`
>
> **Sometimes sent:** `undefined` when the site leaves it out. When the platform took it; absent until then. Format: an ISO 8601 date and time.
>
> **lastAttemptAt** `Date`
>
> **Sometimes sent:** `undefined` when the site leaves it out. The last try; absent before the first. Format: an ISO 8601 date and time.
>
> **nextAttemptAt** `Date | null`
>
> **Sometimes sent:** `undefined` when the site leaves it out, and it can be `null`. Set while `status` is `retry`; null otherwise. Format: an ISO 8601 date and time.
>
> **lastError** `string`
>
> **Sometimes sent:** `undefined` when the site leaves it out. Why the last try failed, as the platform said. At most 500 characters.
>
> **detail** `string`
>
> **Sometimes sent:** `undefined` when the site leaves it out. Why it wasn’t sent, for a status it ended in without trying (e.g. “The event was retracted.”). At most 500 characters.
>
> **test** `boolean`
>
> **Sometimes sent:** `undefined` when the site leaves it out. Present (true) only when it went to the platform’s test mode. Always `true`.
>
> **adjustment** `AdjustmentDeliveryState`
>
> **Sometimes sent:** `undefined` when the site leaves it out. An `AdjustmentDeliveryState`. Telling this platform about the latest adjustment, once it had already received the event. Google Ads: the event re-sent with its new value (a retraction is sent as 0 — Google still counts the conversion). Google Analytics: a `refund`, for purchases only and only to lower the value. Meta, TikTok, Google Tag Manager and browser-only rows cannot be told (`unsupported`, with what to do instead in `detail`).
>
> **AdjustmentDeliveryState**
>
> **adjustmentId** `string`
>
> **Always sent.** Opaque, unique per site, stable. The CMS mints a UUID for events it originates; another system mints its own (e.g. `tenant-42:order:1234:purchase`). Forwarded unchanged to every ad platform as its de-duplication key. Matches `^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$`. At most 128 characters.
>
> **action** `Action`
>
> **Always sent.** `retract` (withdraw the conversion) or `restate` (a new value). One of `retract`, `restate`; a newer site may send others, so keep a fallback.
>
> **status** `AdjustmentDeliveryStateStatus`
>
> **Always sent.** `queued`/`retry` — owed; `sent` — delivered (`test: true` in a platform test mode); `failed` — attempts used up or a permanent error; `not-configured` — credentials or the live Google tag missing; `unsupported` — the platform cannot be told (`detail` says why and what to do instead); `not-needed` — the platform already holds this value. One of `queued`, `retry`, `sent`, `failed`, `not-configured`, `unsupported`, `not-needed`; a newer site may send others, so keep a fallback.
>
> **attempts** `number`
>
> **Always sent.** Sends tried so far. At least 0.
>
> **value** `number`
>
> **Sometimes sent:** `undefined` when the site leaves it out. The value the platform holds once it is sent. At least 0.
>
> **refund** `number`
>
> **Sometimes sent:** `undefined` when the site leaves it out. Google Analytics: the amount refunded. At least 0.
>
> **currency** `string`
>
> **Sometimes sent:** `undefined` when the site leaves it out. ISO 4217 code, e.g. `USD`. Matches `^[A-Z]{3}$`.
>
> **sentAt** `Date`
>
> **Sometimes sent:** `undefined` when the site leaves it out. When the platform took it; absent until then. Format: an ISO 8601 date and time.
>
> **lastAttemptAt** `Date`
>
> **Sometimes sent:** `undefined` when the site leaves it out. The last try; absent before the first. Format: an ISO 8601 date and time.
>
> **nextAttemptAt** `Date | null`
>
> **Sometimes sent:** `undefined` when the site leaves it out, and it can be `null`. Set while `status` is `retry`; null otherwise. Format: an ISO 8601 date and time.
>
> **lastError** `string`
>
> **Sometimes sent:** `undefined` when the site leaves it out. Why the last try failed, as the platform said. At most 500 characters.
>
> **detail** `string`
>
> **Sometimes sent:** `undefined` when the site leaves it out. Why it wasn’t sent, for a status it ended in without trying (e.g. “The event was retracted.”). At most 500 characters.
>
> **test** `boolean`
>
> **Sometimes sent:** `undefined` when the site leaves it out. Present (true) only when it went to the platform’s test mode. Always `true`.
>
> **status** `Status`
>
> **Always sent.** `active`; `retracted` (withdrawn by an adjustment) or `adjusted` (its value restated). One of `active`, `retracted`, `adjusted`; a newer site may send others, so keep a fallback.
>
> **adjustments** `readonly Adjustment[]`
>
> **Always sent.** A list of `Adjustment`. Every adjustment, oldest first (empty when none).
>
> **Adjustment**
>
> **adjustmentId** `string`
>
> **Always sent.** Opaque, unique per site, stable. The CMS mints a UUID for events it originates; another system mints its own (e.g. `tenant-42:order:1234:purchase`). Forwarded unchanged to every ad platform as its de-duplication key. Matches `^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$`. At most 128 characters.
>
> **action** `Action`
>
> **Always sent.** `retract` (withdraw the conversion) or `restate` (a new value). One of `retract`, `restate`; a newer site may send others, so keep a fallback.
>
> **recordedAt** `Date`
>
> **Always sent.** When the site stored it. Format: an ISO 8601 date and time.
>
> **value** `number`
>
> **Sometimes sent:** `undefined` when the site leaves it out. The restated value (restate only). At least 0.
>
> **currency** `string`
>
> **Sometimes sent:** `undefined` when the site leaves it out. ISO 4217 code of `value`. Matches `^[A-Z]{3}$`.
>
> **reason** `string`
>
> **Sometimes sent:** `undefined` when the site leaves it out. Why, as the writer gave it. At most 500 characters.
>
> **occurredAt** `Date`
>
> **Sometimes sent:** `undefined` when the site leaves it out. When the change happened (as the writer said). Format: an ISO 8601 date and time.
>
> **recordedBy** `RecordedBy`
>
> **Sometimes sent:** `undefined` when the site leaves it out. A `RecordedBy`. Who wrote it: an API key (its public key ID and label) or an administrator.
>
> **RecordedBy**
>
> **type** `RecordedByType`
>
> **Always sent.** An API key or a signed-in person. One of `apiKey`, `user`; a newer site may send others, so keep a fallback.
>
> **label** `string`
>
> **Always sent.** The key’s label, or the person’s name.
>
> **keyId** `string`
>
> **Sometimes sent:** `undefined` when the site leaves it out. An API key: its public key id.
>
> **id** `string`
>
> **Sometimes sent:** `undefined` when the site leaves it out. A person: their user `_id`.
>
> **trigger** `ConversionEventTrigger`
>
> **Sometimes sent:** `undefined` when the site leaves it out. A `ConversionEventTrigger`. What fired it.
>
> **ConversionEventTrigger**
>
> **type** `ConversionEventTriggerType`
>
> **Sometimes sent:** `undefined` when the site leaves it out. A form submitted, a page viewed, a link clicked, a chat event, or an `external` fact. One of `form`, `pageView`, `linkClick`, `chat`, `external`; a newer site may send others, so keep a fallback.
>
> **ref** `string`
>
> **Sometimes sent:** `undefined` when the site leaves it out. What exactly: the form’s `aposDocId` (`form`), the page path (`pageView`), the link rule that matched or `outbound:<host>` / `download:<type>` (`linkClick`), the chat event (`chat`), or the producer’s reference (`external`). At most 256 characters.
>
> **recordedAt** `Date`
>
> **Sometimes sent:** `undefined` when the site leaves it out. When the site stored it. Format: an ISO 8601 date and time.
>
> **page** `ConversionEventPageInfo`
>
> **Sometimes sent:** `undefined` when the site leaves it out. A `ConversionEventPageInfo`. The page it happened on.
>
> **ConversionEventPageInfo**
>
> **url** `string`
>
> **Sometimes sent:** `undefined` when the site leaves it out. Analytics or Marketing only. Format: an absolute URL.
>
> **path** `string`
>
> **Sometimes sent:** `undefined` when the site leaves it out. Its path.
>
> **referrer** `string`
>
> **Sometimes sent:** `undefined` when the site leaves it out. Origin only.
>
> **visitorId** `string`
>
> **Sometimes sent:** `undefined` when the site leaves it out. Analytics or Marketing only.
>
> **attribution** `EventAttribution`
>
> **Sometimes sent:** `undefined` when the site leaves it out. An `EventAttribution`. The attribution snapshot the event copied when it happened — independent of the visitor record’s later changes or expiry.
>
> **EventAttribution**
>
> **adPlatforms** `readonly string[]`
>
> **Always sent.** The ad platforms whose click ids the visits carried (empty when none).
>
> **campaign** `Readonly\<Record\<string, string\>\>`
>
> **Always sent.** The campaign parameters in force (`utm_*`, …); empty when none.
>
> **firstTouch** `AttributionTouch | null`
>
> **Sometimes sent:** `undefined` when the site leaves it out, and it can be `null`. An `AttributionTouch`. The visitor’s first visit on record; null when unknown.
>
> **AttributionTouch**
>
> **at** `Date`
>
> **Sometimes sent:** `undefined` when the site leaves it out. When the page view happened. Format: an ISO 8601 date and time.
>
> **path** `string`
>
> **Sometimes sent:** `undefined` when the site leaves it out. The page’s path.
>
> **landingUrl** `string`
>
> **Sometimes sent:** `undefined` when the site leaves it out. The full landing URL. Without Marketing consent its identifier parameters are removed. Format: an absolute URL.
>
> **referrerOrigin** `string | null`
>
> **Sometimes sent:** `undefined` when the site leaves it out, and it can be `null`. Where the visitor came from; null for a direct visit.
>
> **adPlatforms** `readonly AdPlatforms[]`
>
> **Sometimes sent:** `undefined` when the site leaves it out. Platforms whose ad click ID the URL carried — kept for every visitor, consent or not. Each one of `google`, `meta`, `tiktok`, `other`.
>
> **campaign** `Readonly\<Record\<string, string\>\>`
>
> **Sometimes sent:** `undefined` when the site leaves it out. UTM and campaign parameters (`utm_*`, `gad_source`, `gad_campaignid`, `srsltid`).
>
> **clickIds** `Readonly\<Record\<string, string\>\>`
>
> **Sometimes sent:** `undefined` when the site leaves it out. Ad click IDs (`gclid`, `gbraid`, `wbraid`, `fbclid`, `ttclid`). Marketing only.
>
> **linker** `Readonly\<Record\<string, string\>\>`
>
> **Sometimes sent:** `undefined` when the site leaves it out. Google’s cross-domain linker value (`_gl`). Marketing only.
>
> **lastTouch** `AttributionTouch | null`
>
> **Sometimes sent:** `undefined` when the site leaves it out, and it can be `null`. An `AttributionTouch`. Their last visit with an ad click or campaign parameters; null when none. Its fields are listed under the first `AttributionTouch` above.
>
> **clickIds** `Readonly\<Record\<string, string\>\>`
>
> **Sometimes sent:** `undefined` when the site leaves it out. Marketing only.
>
> **browserIds** `Readonly\<Record\<string, string\>\>`
>
> **Sometimes sent:** `undefined` when the site leaves it out. Ad-platform browser IDs (`_gcl_au` — the cross-site key — `_gcl_aw`, `_gcl_gb`, `_gcl_gs`, `_gcl_dc`, `_ga`, `_fbp`, `_fbc`, `_ttp`). Marketing only.
>
> **gaSessions** `Readonly\<Record\<string, string\>\>`
>
> **Sometimes sent:** `undefined` when the site leaves it out. The visitor’s Google Analytics session per data stream when it happened (from the `_ga_<container>` cookies), keyed by the measurement ID without its `G-`. The GA4 server sender joins the event to that session. Marketing only.
>
> **provenance** `Readonly\<Record\<string, Provenance\>\>`
>
> **Sometimes sent:** `undefined` when the site leaves it out. Where each identifier came from: the visitor’s attribution record, the page URL, the platform’s own cookie, or the system that posted an external fact.
>
> **consent** `ConsentSnapshot`
>
> **Sometimes sent:** `undefined` when the site leaves it out. A `ConsentSnapshot`. The consent in force when the event happened. For an external fact (`origin: producer`) the posting system’s flags are recorded as `adUserData` / `adPersonalization` / `gpc` (limited data use).
>
> **ConsentSnapshot**
>
> **categories** `ConsentSnapshotCategories`
>
> **Always sent.** A `ConsentSnapshotCategories`. What the visitor allowed.
>
> **ConsentSnapshotCategories**
>
> **analytics** `boolean`
>
> **Sometimes sent:** `undefined` when the site leaves it out. Analytics cookies and measurement.
>
> **marketing** `boolean`
>
> **Sometimes sent:** `undefined` when the site leaves it out. Marketing: ad platforms and their identifiers.
>
> **mode** `ConsentSnapshotMode`
>
> **Always sent.** The rule that applied to the visitor: `lenient` (a notice-only region: non-essential ran unless they declined) or `strict` (an opt-in region: nothing non-essential until they agreed). One of `lenient`, `strict`; a newer site may send others, so keep a fallback.
>
> **gpc** `boolean`
>
> **Always sent.** Global Privacy Control, or the producer’s limited-data-use flag.
>
> **country** `string | null`
>
> **Sometimes sent:** `undefined` when the site leaves it out, and it can be `null`. The visitor’s two-letter country, when known; null otherwise. Matches `^[A-Z]{2}$`.
>
> **policyVersion** `number | null`
>
> **Sometimes sent:** `undefined` when the site leaves it out, and it can be `null`. The consent policy version the visitor answered; null when none.
>
> **adUserData** `boolean`
>
> **Sometimes sent:** `undefined` when the site leaves it out. An external fact: the producer allowed sending user data to ad platforms.
>
> **adPersonalization** `boolean`
>
> **Sometimes sent:** `undefined` when the site leaves it out. An external fact: the producer allowed ad personalization.
>
> **origin** `string`
>
> **Sometimes sent:** `undefined` when the site leaves it out. Present (`producer`) when the consent came with an external fact. Always `producer`.
>
> **value** `number`
>
> **Sometimes sent:** `undefined` when the site leaves it out. Its value, when the Conversion has one. At least 0.
>
> **adjustedValue** `number`
>
> **Sometimes sent:** `undefined` when the site leaves it out. The latest restated value. At least 0.
>
> **currency** `string`
>
> **Sometimes sent:** `undefined` when the site leaves it out. ISO 4217 code of `value`, e.g. `USD`. Matches `^[A-Z]{3}$`.
>
> **userData** `UserDataHashes`
>
> **Sometimes sent:** `undefined` when the site leaves it out. An `UserDataHashes`. Hashed contact details. Email: trimmed + lower-cased; `emailGoogleSha256` only for a Gmail address with dots (Google drops them). Phone: E.164 — `phoneSha256` with the "+", `phoneDigitsSha256` without (Meta’s form).
>
> **UserDataHashes**
>
> **emailSha256** `string`
>
> **Sometimes sent:** `undefined` when the site leaves it out. Lower-case hex SHA-256 digest. Matches `^[0-9a-f]{64}$`.
>
> **emailGoogleSha256** `string`
>
> **Sometimes sent:** `undefined` when the site leaves it out. Lower-case hex SHA-256 digest. Matches `^[0-9a-f]{64}$`.
>
> **phoneSha256** `string`
>
> **Sometimes sent:** `undefined` when the site leaves it out. Lower-case hex SHA-256 digest. Matches `^[0-9a-f]{64}$`.
>
> **phoneDigitsSha256** `string`
>
> **Sometimes sent:** `undefined` when the site leaves it out. Lower-case hex SHA-256 digest. Matches `^[0-9a-f]{64}$`.
>
> **submissionId** `string`
>
> **Sometimes sent:** `undefined` when the site leaves it out. The form submission a form conversion came from.
>
> **producer** `Producer`
>
> **Sometimes sent:** `undefined` when the site leaves it out. A `Producer`. The system that posted an external fact.
>
> **Producer**
>
> **system** `string`
>
> **Always sent.** The system’s name, as it posted it (lowercase, `a-z0-9._-`, at most 64).
>
> **contentHash** `string`
>
> **Always sent.** Fingerprint of the posted fact (idempotency).
>
> **recordedBy** `RecordedBy`
>
> **Always sent.** A `RecordedBy`. Who wrote it: an API key (its public key ID and label) or an administrator. Its fields are listed under the first `RecordedBy` above.
>
> **ref** `string`
>
> **Sometimes sent:** `undefined` when the site leaves it out. Its own reference for the fact (at most 256 characters), when it gave one.
>
> **join** `ConversionEventJoin`
>
> **Sometimes sent:** `undefined` when the site leaves it out. A `ConversionEventJoin`. How an external fact was joined to a visitor (`by: null` — no match).
>
> **ConversionEventJoin**
>
> **by** `By | null`
>
> **Sometimes sent:** `undefined` when the site leaves it out, and it can be `null`. The identifier that matched, or null for none. One of `visitorId`, `token`, `gclAu`, `gclid`, `gbraid`, `wbraid`, `fbclid`, `ttclid`, `emailSha256`, `phoneSha256`; a newer site may send others, so keep a fallback.

> [accordion] Errors
>
> Nothing to catch: the SDK retries what is safe, and throws the rest as the classes below. Your exception handler turns them into your app’s own answers, once for every call (see [Map SDK errors to responses](https://docs.fetchski-attract.com/adonisjs/calling-the-api.md#map-sdk-errors-to-responses)); catch only what you want to react to here.
>
> **AuthenticationFailed** `401`
>
> The API key is missing, malformed, unknown, disabled, expired or archived (`invalid`), or a request that must be signed — by the route, or by the key’s own signing mode — has a missing, stale or wrong signature (an upload signs its SHA-256, declared in `X-Fetchski-Content-SHA256`, in the body’s place) (`unauthorized`).
>
> **Retried first:** No. **Read:** `siteMessage`, `errorName`, `data`.
>
> **Forbidden** `403`
>
> The API key lacks the scope this route needs (the message names it), or the key is used from an address outside its allowlist.
>
> **Retried first:** No. **Read:** `siteMessage`, `errorName`, `data`.
>
> **NotFound** `404`
>
> Nothing with this identifier — or the caller may not use the Data API at all (anonymous visitors and non-admin sessions get 404 so the API is not advertised). A write while conversion tracking is switched off also answers 404.
>
> **Retried first:** No. **Read:** `siteMessage`, `errorName`, `data`.
>
> **RateLimited** `429`
>
> The key used up its reads- or writes-per-minute limit (set per key; defaults 600 / 120). Still, after the client’s own waits and retries.
>
> **Retried first:** Yes: the client waits `Retry-After` (1 s when absent; over 60 s it gives up at once) and retries, up to `retries` times (2 by default). **Read:** `retryAfter` (seconds, or null), `siteMessage`.
>
> **SiteError** `5xx`
>
> The site failed (or something in front of it did). Its logs say why.
>
> **Retried first:** A 502, 503 or 504: yes, after 0.5 s then 1 s. A 500: no. **Read:** `siteMessage`, `errorName`, `data`.
>
> **SiteUnreachable** `no answer`
>
> No answer: DNS, a refused connection, TLS, or the timeout (15 s per attempt by default).
>
> **Retried first:** Yes, after 0.5 s then 1 s. **Read:** `reason`, `neverConnected`.
>
> **RequestNotSent** `not sent`
>
> The client refused to send it (e.g. a path segment of `..`); the site never saw it.
>
> **Retried first:** No. **Read:** `reason` (what to change).
>
> **SiteRequestFailed** `any other`
>
> Any other status, or a success whose JSON doesn’t match this SDK version’s types (update the SDK).
>
> **Retried first:** No. **Read:** `reason`, `status`.
>
> **UnknownSite** `setup`
>
> `sites.forSite('acme')` named a slug `config/attract.ts` doesn’t list (with the keys in the database: no row in `attract_api_keys`).
>
> **Retried first:** No: nothing was sent. **Read:** `message`, `slug`.
>
> **SiteConfigurationError** `setup`
>
> The site’s entry is wrong: a malformed `baseUrl` or key, a key filed under another slug, or no default site. `node ace attract:check` names the fix.
>
> **Retried first:** No: nothing was sent. **Read:** `message` (what to fix).

## Behavior

* **Access:** an API key with `conversions:read` — or its group master `ads:all`, or `admin:all` — or a signed-in user holding the `conversion-events:view` permission. Anonymous callers get 404.
* **Rate limit:** the key’s own reads-per-minute limit, set when the key is created (default 600); over it, 429 with `Retry-After`. The client waits and retries for you; you see `RateLimited` only if that runs out.
* **Retries:** a read is safe to repeat: the client retries it on no answer, a 502/503/504 and a 429.
* **Timeout:** 15 s per attempt by default; raise `timeout` (milliseconds) in `config/attract.ts` for longer.

## More examples

```ts Or null wrap theme={"dark"}
import app from '@adonisjs/core/services/app'
import { SiteClient } from '@fetchski/attract-cms-sdk'

const site = await app.container.make(SiteClient)
const getConversionEvent = await site.conversionEvents().findConversionEvent({ eventId: 'tenant-42:order:1234:purchase' }) // GetConversionEventResponse | null: null when the site answers 404
```

## Test it

It calls the controller above with the SDK faked: `fakeAttract()` puts the fake in the container for this one test, and it answers every call with a valid example, so nothing leaves your machine and no key is read. Save it as `tests/unit/conversion_events_get_conversion_event.spec.ts`, then run your tests:

```ts tests/unit/conversion_events_get_conversion_event.spec.ts theme={"dark"}
import { test } from '@japa/runner'
import app from '@adonisjs/core/services/app'
import testUtils from '@adonisjs/core/services/test_utils'
import { fakeAttract } from '@fetchski/attract-cms-adonis/testing'
import GetConversionEventController from '#controllers/get_conversion_event_controller'

test('conversionEvents.getConversionEvent', async () => {
  const site = fakeAttract()

  const ctx = await testUtils.createHttpContext()
  ctx.params = { eventId: 'tenant-42:order:1234:purchase' }
  const controller = await app.container.make(GetConversionEventController)
  await controller.handle(ctx)

  site.assertCalled('conversionEvents.getConversionEvent', (args) => args.eventId === 'tenant-42:order:1234:purchase')
})
```

```bash Terminal theme={"dark"}
node ace test
```

**Answer with your own values:** fake `conversionEvents.getConversionEvent` with `factories.GetConversionEventResponse({...})`, the fields you give over a valid example (nested ones too).

```ts tests/unit/conversion_events_get_conversion_event_answers.spec.ts theme={"dark"}
import { test } from '@japa/runner'
import { factories, fakeAttract } from '@fetchski/attract-cms-adonis/testing'

test('conversionEvents.getConversionEvent answers your values', async ({ assert }) => {
  const site = fakeAttract({
    'conversionEvents.getConversionEvent': factories.GetConversionEventResponse({ event: { eventId: 'Your value' } }),
  })

  const getConversionEvent = await site.conversionEvents().getConversionEvent({ eventId: 'tenant-42:order:1234:purchase' })
  assert.containSubset(getConversionEvent, { event: { eventId: 'Your value' } })
})
```

## Related

> [card] [List conversion events (incremental sync)](https://docs.fetchski-attract.com/adonisjs/reference/conversion-events/list-conversion-events.md)
>
> `site.conversionEvents().listConversionEvents()`

> [card] [Post a conversion another system saw](https://docs.fetchski-attract.com/adonisjs/reference/conversion-events/create-external-conversion-event.md)
>
> `site.conversionEvents().createExternalConversionEvent()`

> [card] [Retract or restate a conversion event](https://docs.fetchski-attract.com/adonisjs/reference/conversion-events/adjust-conversion-event.md)
>
> `site.conversionEvents().adjustConversionEvent()`

> [card] [GET /conversion-event/\{eventId}](https://docs.fetchski-attract.com/api-reference/conversion-events/get-one-conversion-event.md)
>
> The endpoint this calls, in the API reference: the HTTP request and answer, and every rule the site checks.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.