Conversion API
The Conversion API lets you report non-Stripe conversions to Selgeo -- signups, form submissions, free trials, upgrades, or any custom event. You call it from your server after a conversion happens, and Selgeo attributes it to the referring partner based on a click_id or promo_code.
API Version: v1
Endpoint: POST /api/v1/conversions
Prerequisites
- The Selgeo snippet is installed on your website (Snippet Setup) to capture
click_idvalues - A secret API key (
sk_test_*orsk_live_*) from the Selgeo dashboard (Settings > API Keys)
Authentication
The Conversion API uses secret API keys passed as a Bearer token in the Authorization header:
Authorization: Bearer sk_test_YOUR_KEY
| Key prefix | Mode | Description |
|---|---|---|
sk_test_* | Test | Test mode -- conversions are tracked but no real commissions |
sk_live_* | Live | Live mode -- conversions generate real commissions |
Secret keys (sk_*) must only be used in server-side code. Never include them in JavaScript that runs in the browser, mobile apps, or any client-side code. Use public keys (pk_*) for the tracking snippet.
Request format
POST /api/v1/conversions
Content-Type: application/json
Authorization: Bearer sk_test_YOUR_KEY
Request body
| Field | Type | Required | Description |
|---|---|---|---|
click_id | string (UUID) | Conditional | The click ID from the tracking snippet. Required if promo_code is not provided. |
promo_code | string | Conditional | A promo code for attribution. Required if click_id is not provided. |
external_transaction_id | string | Yes | Your unique identifier for this conversion (e.g., order ID, signup ID). Used for deduplication. Max 255 characters. |
event_type | string | Yes | The type of conversion event (e.g., signup, purchase, upgrade, trial_start). Max 100 characters. |
amount_cents | integer | No | The conversion value in cents. Default: 0. |
currency | string | Conditional | ISO 4217 3-letter currency code (e.g., EUR, USD). Required when amount_cents > 0. |
occurred_at | string (ISO 8601) | No | When the conversion occurred. Defaults to the current time. |
prospect_email | string (email) | No | The converting user's email address. Used for self-referral fraud detection. |
metadata | object | No | Arbitrary key-value pairs for your own use. Max 4 KB. |
external_transaction_id, not external_idSelgeo uses snake_case for every JSON field. Integrators sometimes guess external_id or transactionId — those are rejected with HTTP 400 and a details entry whose path is "external_transaction_id". The same applies to event_type, amount_cents, promo_code, and prospect_email.
You must provide either click_id or promo_code (or both). If neither is provided, the API returns a 422 error.
Response format
A successful response returns 201 Created with the conversion details and attribution decision:
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"source": "conversion_api",
"event_type": "signup",
"external_transaction_id": "signup_12345",
"amount_cents": 0,
"currency": null,
"occurred_at": "2026-04-02T10:30:00.000Z",
"is_test": true,
"attributed": true,
"attribution": {
"attribution_event_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"attribution_source": "link",
"participant_id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"explanation": "Attributed to partner via tracking link click"
},
"created_at": "2026-04-02T10:30:01.000Z"
}
Response fields
| Field | Type | Description |
|---|---|---|
id | string (UUID) | The conversion record ID. null if attribution failed. |
source | string | Always "conversion_api" for this endpoint. |
event_type | string | Echoes back your event_type. |
external_transaction_id | string | Echoes back your transaction ID. |
amount_cents | integer | The conversion value. |
currency | string | null | The currency code. |
occurred_at | string | When the conversion occurred. |
is_test | boolean | true if using a test key, false if using a live key. |
attributed | boolean | true if the conversion was attributed to a partner. |
attribution.attribution_event_id | string | null | The attribution event ID. null if not attributed. |
attribution.attribution_source | string | null | "link" (click-based) or "code" (promo code). null if not attributed. |
attribution.participant_id | string | null | The partner's participant ID. null if not attributed. |
attribution.explanation | string | A human-readable explanation of the attribution decision. |
Interpreting the attribution decision
attributed | attribution_source | Meaning |
|---|---|---|
true | "link" | Conversion attributed to a partner via a tracking link click. |
true | "code" | Conversion attributed to a partner via a promo code. |
false | null | No attribution. The explanation field describes why (e.g., expired click, invalid promo code, partner not active). |
Attribution priority
When both click_id and promo_code are provided in the same request, promo code takes priority:
- Selgeo first attempts attribution via
promo_code. - If the promo code is valid and linked to an active partner, the conversion is attributed via
"code". - If the promo code is invalid or inactive, Selgeo falls back to
click_idattribution.
This priority order ensures that partners who distribute promo codes get credit even when the customer also arrived through a different partner's tracking link.
Examples
Basic conversion with click_id
- curl
- JavaScript (Node.js)
- Python
- PHP
curl -X POST https://api.selgeo.com/api/v1/conversions \
-H "Authorization: Bearer sk_test_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"click_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"external_transaction_id": "signup_12345",
"event_type": "signup",
"amount_cents": 0
}'
const response = await fetch('https://api.selgeo.com/api/v1/conversions', {
method: 'POST',
headers: {
'Authorization': 'Bearer sk_test_YOUR_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
click_id: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
external_transaction_id: 'signup_12345',
event_type: 'signup',
amount_cents: 0,
}),
});
const conversion = await response.json();
console.log('Attributed:', conversion.attributed);
console.log('Partner:', conversion.attribution.participant_id);
import requests
response = requests.post(
"https://api.selgeo.com/api/v1/conversions",
headers={
"Authorization": "Bearer sk_test_YOUR_KEY",
"Content-Type": "application/json",
},
json={
"click_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"external_transaction_id": "signup_12345",
"event_type": "signup",
"amount_cents": 0,
},
)
conversion = response.json()
print(f"Attributed: {conversion['attributed']}")
print(f"Partner: {conversion['attribution']['participant_id']}")
$ch = curl_init('https://api.selgeo.com/api/v1/conversions');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer sk_test_YOUR_KEY',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'click_id' => 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
'external_transaction_id' => 'signup_12345',
'event_type' => 'signup',
'amount_cents' => 0,
]),
]);
$response = curl_exec($ch);
$conversion = json_decode($response, true);
echo "Attributed: " . ($conversion['attributed'] ? 'yes' : 'no') . "\n";
echo "Partner: " . $conversion['attribution']['participant_id'] . "\n";
curl_close($ch);
Paid conversion with amount
- curl
- JavaScript (Node.js)
- Python
- PHP
curl -X POST https://api.selgeo.com/api/v1/conversions \
-H "Authorization: Bearer sk_test_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"click_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"external_transaction_id": "order_67890",
"event_type": "purchase",
"amount_cents": 9900,
"currency": "EUR"
}'
const response = await fetch('https://api.selgeo.com/api/v1/conversions', {
method: 'POST',
headers: {
'Authorization': 'Bearer sk_test_YOUR_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
click_id: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
external_transaction_id: 'order_67890',
event_type: 'purchase',
amount_cents: 9900,
currency: 'EUR',
}),
});
const conversion = await response.json();
import requests
response = requests.post(
"https://api.selgeo.com/api/v1/conversions",
headers={
"Authorization": "Bearer sk_test_YOUR_KEY",
"Content-Type": "application/json",
},
json={
"click_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"external_transaction_id": "order_67890",
"event_type": "purchase",
"amount_cents": 9900,
"currency": "EUR",
},
)
conversion = response.json()
$ch = curl_init('https://api.selgeo.com/api/v1/conversions');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer sk_test_YOUR_KEY',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'click_id' => 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
'external_transaction_id' => 'order_67890',
'event_type' => 'purchase',
'amount_cents' => 9900,
'currency' => 'EUR',
]),
]);
$response = curl_exec($ch);
$conversion = json_decode($response, true);
curl_close($ch);
Promo code attribution
- curl
- JavaScript (Node.js)
- Python
- PHP
curl -X POST https://api.selgeo.com/api/v1/conversions \
-H "Authorization: Bearer sk_test_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"promo_code": "PARTNER20",
"external_transaction_id": "order_99999",
"event_type": "purchase",
"amount_cents": 4900,
"currency": "EUR"
}'
const response = await fetch('https://api.selgeo.com/api/v1/conversions', {
method: 'POST',
headers: {
'Authorization': 'Bearer sk_test_YOUR_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
promo_code: 'PARTNER20',
external_transaction_id: 'order_99999',
event_type: 'purchase',
amount_cents: 4900,
currency: 'EUR',
}),
});
const conversion = await response.json();
// conversion.attribution.attribution_source === "code"
import requests
response = requests.post(
"https://api.selgeo.com/api/v1/conversions",
headers={
"Authorization": "Bearer sk_test_YOUR_KEY",
"Content-Type": "application/json",
},
json={
"promo_code": "PARTNER20",
"external_transaction_id": "order_99999",
"event_type": "purchase",
"amount_cents": 4900,
"currency": "EUR",
},
)
conversion = response.json()
# conversion["attribution"]["attribution_source"] == "code"
$ch = curl_init('https://api.selgeo.com/api/v1/conversions');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer sk_test_YOUR_KEY',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'promo_code' => 'PARTNER20',
'external_transaction_id' => 'order_99999',
'event_type' => 'purchase',
'amount_cents' => 4900,
'currency' => 'EUR',
]),
]);
$response = curl_exec($ch);
$conversion = json_decode($response, true);
// $conversion['attribution']['attribution_source'] === 'code'
curl_close($ch);
Both click_id and promo_code
When both are provided, the promo code takes priority:
curl -X POST https://api.selgeo.com/api/v1/conversions \
-H "Authorization: Bearer sk_test_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"click_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"promo_code": "PARTNER20",
"external_transaction_id": "order_55555",
"event_type": "purchase",
"amount_cents": 4900,
"currency": "EUR"
}'
If PARTNER20 is valid, attribution_source will be "code". If PARTNER20 is invalid or inactive, Selgeo falls back to click_id and attribution_source will be "link".
Passing click_id from frontend to backend
A common pattern is to read the click_id from the snippet on the frontend and send it with the form submission or API call:
// Frontend: include click_id in your form submission
const form = document.getElementById('signup-form');
form.addEventListener('submit', (event) => {
event.preventDefault();
const clickId = __selgeo.getClickId();
const formData = new FormData(form);
fetch('/api/signup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: formData.get('email'),
name: formData.get('name'),
clickId: clickId, // may be null
}),
});
});
Then on your backend, after processing the signup, call the Conversion API:
// Backend: report the conversion to Selgeo
app.post('/api/signup', async (req, res) => {
const { email, name, clickId } = req.body;
// 1. Process the signup in your system
const user = await createUser({ email, name });
// 2. Report to Selgeo (only if clickId is present)
if (clickId) {
await fetch('https://api.selgeo.com/api/v1/conversions', {
method: 'POST',
headers: {
'Authorization': 'Bearer sk_test_YOUR_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
click_id: clickId,
external_transaction_id: `signup_${user.id}`,
event_type: 'signup',
amount_cents: 0,
}),
});
}
res.json({ success: true });
});
If you use traditional form submissions (not AJAX), add a hidden input that the snippet populates:
<form action="/api/signup" method="POST">
<input type="email" name="email" required />
<input type="hidden" name="clickId" id="selgeo-click-id" />
<button type="submit">Sign Up</button>
</form>
<script>
// Set the hidden field when the form loads
document.addEventListener('DOMContentLoaded', () => {
const clickId = typeof __selgeo !== 'undefined'
? __selgeo.getClickId()
: null;
if (clickId) {
document.getElementById('selgeo-click-id').value = clickId;
}
});
</script>
Error handling
HTTP status codes
| Status | Meaning | Action |
|---|---|---|
201 | Conversion created successfully | Read the attributed field to check if it was attributed |
400 | Validation error (missing/invalid fields) | Fix the request body per the error details |
401 | Invalid or missing API key | Check your Authorization header and key |
409 | Duplicate external_transaction_id | This conversion was already reported. This is idempotent -- no action needed |
422 | No attribution signal or test/live mode mismatch | Either provide click_id/promo_code, or ensure the key mode matches (test click_id with test key) |
429 | Rate limit exceeded | Back off and retry. Check the Retry-After header |
Error response format
{
"statusCode": 400,
"message": "Validation failed",
"error": "Bad Request",
"code": "VALIDATION_ERROR"
}
Rate limits
The Conversion API is rate-limited to 120 requests per minute per secret key prefix. If you exceed this limit, the API returns 429 Too Many Requests with a Retry-After header indicating how many seconds to wait.
For high-volume integrations, batch your conversions or spread them over time. If you consistently need higher limits, contact support.
Deduplication
The external_transaction_id field is used for deduplication. If you send the same external_transaction_id twice with the same secret key, the second request returns 409 Conflict. This makes the API safe to retry on network failures -- you will not create duplicate conversions.
Mode mismatch
Test-mode keys (sk_test_*) can only attribute conversions to click IDs generated by test-mode public keys (pk_test_*). Similarly, live-mode keys only work with live-mode click IDs. If there is a mismatch, the API returns 422 with code MODE_MISMATCH.
Common pitfalls
| Symptom | Cause | Fix |
|---|---|---|
400 with a details entry whose path is "external_transaction_id" | You sent external_id or transactionId instead of the canonical name | Rename the field to external_transaction_id |
400 with a details entry whose path is "currency" | You set amount_cents > 0 but did not include currency | Add a 3-letter ISO 4217 currency value (e.g. "EUR") |
422 with code: "NO_ATTRIBUTION_SIGNAL" | Neither click_id nor promo_code was provided | Include at least one. See Passing click_id from frontend to backend |
422 with code: "MODE_MISMATCH" | You used sk_test_* with a live-mode click_id (or vice versa) | Match the secret-key mode to the click ID mode |
409 with code: "DUPLICATE_EXTERNAL_TRANSACTION_ID" | The same external_transaction_id was already accepted | This is idempotent — treat as success (or generate a unique ID per real conversion) |
For reference, the 400 envelope shape returned by the validation pipe is:
{
"statusCode": 400,
"message": "<joined per-field messages>",
"error": "Bad Request",
"code": "VALIDATION_ERROR",
"details": [
{ "path": "external_transaction_id", "message": "Required" }
]
}
(path is dot-joined for nested fields; details is a flat array.)
Testing
- Set up a test program with a partner and tracking link in the Selgeo dashboard.
- Visit your site via the tracking link to register a click.
- Read the click_id from the browser console:
__selgeo.getClickId()
- Send a test conversion from your terminal:
curl -X POST https://api.selgeo.com/api/v1/conversions \-H "Authorization: Bearer sk_test_YOUR_KEY" \-H "Content-Type: application/json" \-d '{"click_id": "PASTE_CLICK_ID_HERE","external_transaction_id": "test_001","event_type": "signup","amount_cents": 0}'
- Check the response --
attributedshould betrueandattribution_sourceshould be"link". - Verify in the dashboard -- the conversion should appear in Analytics, attributed to the test partner.
Next steps
- Webhooks -- receive notifications when conversions are attributed and commissions are created
- Stripe Metadata -- if you also use Stripe for some conversions
- Test Mode -- detailed guide for testing your integration
- Troubleshooting -- common issues and solutions