Direct Tracking Endpoint
Send tracking events directly to /v1/track without a Convert SDK
Most integrations use one of the Convert SDKs to send tracking events. If you cannot, or prefer not to, use an SDK — for example because your stack uses a language we do not yet publish an SDK for, or because you have a server-to-server pipeline that crafts its own HTTP — you can call the Convert tracking endpoint directly.
This page covers what the request needs to look like and the one Convert-specific requirement that has no equivalent on the SDK path: identifying your traffic with a User-Agent header so the tracking server does not treat your requests as bot traffic.
Prefer to try it interactively? The Send Tracking page in the API Reference has a Try It panel that submits a real request to the live endpoint, with the required header pre-filled.
When this applies
You only need this page if both of the following are true:
- You are building the tracking request yourself instead of letting a Convert SDK build it for you.
- Your HTTP client uses its default User-Agent (for example
node,undici,axios/X,node-fetch/X,GuzzleHttp/X,python-requests/X,Java/X,okhttp/X,Apache-HttpClient/X).
If you are using a Convert SDK, the SDK already identifies its traffic correctly and there is nothing extra to do on your side. See Tracking Conversions for the SDK path.
Required: identify your traffic
Convert's tracking server applies bot detection to every incoming request before any tracking work happens. Default User-Agent strings used by server-side HTTP clients look like bot traffic to that filter and are silently dropped. The server returns 200 OK and your client believes the event was accepted, but the event never reaches your reports or Live Logs.
To opt out of the bot check, set the User-Agent request header to a value that starts with ConvertAgent/<version>. The exact version is up to you and is only used to identify your integration in logs.
ConvertAgent/1.0You can embed ConvertAgent/<version> inside a longer User-Agent if you also want to keep your own client identifier:
MyApp/2.5 ConvertAgent/1.0 (production)The trailing slash in ConvertAgent/ is required — bare ConvertAgent without a version token does not qualify. This prevents accidental matches against unrelated clients that happen to include the word in their User-Agent.
Endpoint shape
Send a POST request to:
https://metrics.convertexperiments.com/v1/track/<sdkKey>The request body follows the same JSON shape every Convert SDK uses internally. Required top-level fields:
| Field | Type | Description |
|---|---|---|
accountId | string or number | The Convert account ID that owns the project |
projectId | string or number | The Convert project ID |
visitors | array | One entry per visitor (see below) |
Each entry inside visitors carries:
| Field | Type | Description |
|---|---|---|
visitorId | string | The unique visitor identifier you are tracking |
events | array | One entry per event for that visitor (bucketing or conversion) |
segments | object | Optional segment data (browser, country, source, campaign, custom) |
Per-event shape:
eventType | data | Notes |
|---|---|---|
bucketing | { experienceId, variationId } | Records that a visitor was bucketed into a variation |
conversion | { goalId, goalData?, bucketingData? } | Records a goal completion |
Refer to the API Reference for the full schema, optional fields, and response codes.
Examples
A minimal bucketing event posted with the required User-Agent:
await fetch(
`https://metrics.convertexperiments.com/v1/track/${sdkKey}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'User-Agent': 'ConvertAgent/1.0',
'Authorization': `Bearer ${sdkKeySecret}`
},
body: JSON.stringify({
accountId,
projectId,
visitors: [
{
visitorId,
events: [
{
eventType: 'bucketing',
data: {
experienceId: String(experienceId),
variationId: String(variationId)
}
}
]
}
]
})
}
);$body = json_encode([
'accountId' => $accountId,
'projectId' => $projectId,
'visitors' => [
[
'visitorId' => $visitorId,
'events' => [
[
'eventType' => 'bucketing',
'data' => [
'experienceId' => (string) $experienceId,
'variationId' => (string) $variationId,
],
],
],
],
],
]);
$request = $requestFactory
->createRequest('POST', "https://metrics.convertexperiments.com/v1/track/{$sdkKey}")
->withHeader('Content-Type', 'application/json')
->withHeader('User-Agent', 'ConvertAgent/1.0')
->withHeader('Authorization', "Bearer {$sdkKeySecret}")
->withBody($streamFactory->createStream($body));
$httpClient->sendRequest($request);curl -X POST "https://metrics.convertexperiments.com/v1/track/${SDK_KEY}" \
-H "Content-Type: application/json" \
-H "User-Agent: ConvertAgent/1.0" \
-H "Authorization: Bearer ${SDK_KEY_SECRET}" \
-d '{
"accountId": "'"${ACCOUNT_ID}"'",
"projectId": "'"${PROJECT_ID}"'",
"visitors": [{
"visitorId": "'"${VISITOR_ID}"'",
"events": [{
"eventType": "bucketing",
"data": { "experienceId": "100123", "variationId": "100456" }
}]
}]
}'Verifying your integration
After your first event lands, open the project's Live Logs in the Convert dashboard. If you see your bucketing or conversion entries appear within a few seconds, the request is being accepted and attributed correctly. If Live Logs stay empty:
- Confirm the
User-Agentheader on every outbound request containsConvertAgent/<version>. A single missing header is enough for the bot filter to silently drop the request. - Confirm
accountId,projectId,visitorId,experienceId, andvariationIdexactly match values that exist in your project. Events referencing experience IDs that are not in the project's active experiments are rejected downstream. - See Troubleshooting for broader diagnostic steps.
When to use an SDK instead
The direct endpoint is the lowest-friction path for languages we do not yet ship an SDK for, or for cases where you want full control over the wire format. For everything else, an SDK handles bucketing, batching, retries, deduplication, and the User-Agent convention automatically. The available SDKs are listed in the Quickstarts.
Updated 3 days ago