Skip to main content

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_id values
  • A secret API key (sk_test_* or sk_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 prefixModeDescription
sk_test_*TestTest mode -- conversions are tracked but no real commissions
sk_live_*LiveLive mode -- conversions generate real commissions
Never expose secret keys in frontend code

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

FieldTypeRequiredDescription
click_idstring (UUID)ConditionalThe click ID from the tracking snippet. Required if promo_code is not provided.
promo_codestringConditionalA promo code for attribution. Required if click_id is not provided.
external_transaction_idstringYesYour unique identifier for this conversion (e.g., order ID, signup ID). Used for deduplication. Max 255 characters.
event_typestringYesThe type of conversion event (e.g., signup, purchase, upgrade, trial_start). Max 100 characters.
amount_centsintegerNoThe conversion value in cents. Default: 0.
currencystringConditionalISO 4217 3-letter currency code (e.g., EUR, USD). Required when amount_cents > 0.
occurred_atstring (ISO 8601)NoWhen the conversion occurred. Defaults to the current time.
prospect_emailstring (email)NoThe converting user's email address. Used for self-referral fraud detection.
metadataobjectNoArbitrary key-value pairs for your own use. Max 4 KB.
Field name is external_transaction_id, not external_id

Selgeo 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.

At least one attribution signal is required

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

FieldTypeDescription
idstring (UUID)The conversion record ID. null if attribution failed.
sourcestringAlways "conversion_api" for this endpoint.
event_typestringEchoes back your event_type.
external_transaction_idstringEchoes back your transaction ID.
amount_centsintegerThe conversion value.
currencystring | nullThe currency code.
occurred_atstringWhen the conversion occurred.
is_testbooleantrue if using a test key, false if using a live key.
attributedbooleantrue if the conversion was attributed to a partner.
attribution.attribution_event_idstring | nullThe attribution event ID. null if not attributed.
attribution.attribution_sourcestring | null"link" (click-based) or "code" (promo code). null if not attributed.
attribution.participant_idstring | nullThe partner's participant ID. null if not attributed.
attribution.explanationstringA human-readable explanation of the attribution decision.

Interpreting the attribution decision

attributedattribution_sourceMeaning
true"link"Conversion attributed to a partner via a tracking link click.
true"code"Conversion attributed to a partner via a promo code.
falsenullNo 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:

  1. Selgeo first attempts attribution via promo_code.
  2. If the promo code is valid and linked to an active partner, the conversion is attributed via "code".
  3. If the promo code is invalid or inactive, Selgeo falls back to click_id attribution.

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 -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
}'
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"
}'

Promo code attribution

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"
}'

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 });
});
You can also use a hidden form field

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

StatusMeaningAction
201Conversion created successfullyRead the attributed field to check if it was attributed
400Validation error (missing/invalid fields)Fix the request body per the error details
401Invalid or missing API keyCheck your Authorization header and key
409Duplicate external_transaction_idThis conversion was already reported. This is idempotent -- no action needed
422No attribution signal or test/live mode mismatchEither provide click_id/promo_code, or ensure the key mode matches (test click_id with test key)
429Rate limit exceededBack 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

SymptomCauseFix
400 with a details entry whose path is "external_transaction_id"You sent external_id or transactionId instead of the canonical nameRename the field to external_transaction_id
400 with a details entry whose path is "currency"You set amount_cents > 0 but did not include currencyAdd a 3-letter ISO 4217 currency value (e.g. "EUR")
422 with code: "NO_ATTRIBUTION_SIGNAL"Neither click_id nor promo_code was providedInclude 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 acceptedThis 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

  1. Set up a test program with a partner and tracking link in the Selgeo dashboard.
  2. Visit your site via the tracking link to register a click.
  3. Read the click_id from the browser console:
    __selgeo.getClickId()
  4. 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
    }'
  5. Check the response -- attributed should be true and attribution_source should be "link".
  6. 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