SEOMadman
SEOMadman docsStart free

Developer

Conversions API

Post completed orders from your server so revenue lands against its true source.

What it is for

The tag on your pages measures visits. It cannot see an order, because the order happens on your server. This endpoint is how you tell us one happened, so a conversion and its revenue land against the source that brought the visit.

Call it once per completed order, from your backend. Never from the browser: it is signed with a secret, and a secret in page source is not a secret.

Before you start

Collecting from your own site is part of the paid plans. It is the one capability whose cost follows your traffic, so it stays with an active subscription: if the subscription lapses, collection pauses and this endpoint answers 402. Data already collected stays yours, and collection resumes when the subscription is active again.

Credentials

Adding a site in Settings gives you two values that are not interchangeable:

  • Site key - public. It ships inside the snippet on your pages. It identifies a site.
  • Secret - private. It is stored only as a digest, so it is shown once, at creation. It

authorizes revenue.

Lost the secret? Add the site again and replace the snippet. We cannot recover it, by design.

The request

POST https://seomadman.com/api/collect/conversion
Content-Type: application/json

{
  "k":  "your-site-key",
  "s":  "your-secret",
  "p":  "/checkout/complete",
  "c":  "ai",
  "v":  99.5,
  "id": "order-10024"
}
FieldRequiredMeaning
kyesSite key from Settings
syesSecret from Settings
pnoPage the conversion belongs to, defaults to /
cyesSource class: ai, search, social, referral, direct
vnoOrder value in your currency, defaults to 0
idyesYour order id, deduplicates retries

Where c comes from

This is the field people ask about. We refuse to guess it: your backend knows which visit converted, we do not, and inventing a source would put an estimate in a column that reads as exact.

The snippet already worked the class out and parked it in session storage under _ac. Read it in the browser and send it along with the order:

// in your checkout form or fetch payload
const source = sessionStorage.getItem("_ac") || "direct";

If checkout happens on a different domain from the tagged pages, session storage does not travel: carry the value yourself, in the URL or in your own session record.

Retries and duplicates

id is required because payment webhooks retry. Send the same id again and the conversion is recorded once. This is checked before anything else is counted, so a retry storm costs you nothing and cannot inflate a figure.

Responses

CodeBodyMeaning
202{"status":"ok"}Recorded
200{"status":"duplicate"}Already recorded under this id
400{"status":"invalid"}Missing id, or c is not a valid class
401{"status":"unknown"}Site key or secret is wrong
402{"status":"unpaid"}Subscription is not active, collection is paused
429{"status":"capped"}Monthly cap reached, retry later
503{"status":"error"}Temporary fault, safe to retry

A wrong key and a wrong secret answer identically, so neither can be found by probing. Retrying any failure is safe, because id makes the call idempotent.

Worked example

curl -X POST https://seomadman.com/api/collect/conversion \
  -H 'content-type: application/json' \
  -d '{"k":"SITE_KEY","s":"SECRET","p":"/checkout/complete","c":"ai","v":99.5,"id":"order-10024"}'

In a Stripe webhook handler, send it after you have verified Stripe's signature and the payment has actually succeeded, using the payment intent id as id:

await fetch("https://seomadman.com/api/collect/conversion", {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({
    k: process.env.SEOMADMAN_SITE_KEY,
    s: process.env.SEOMADMAN_SECRET,
    p: "/checkout/complete",
    c: order.source || "direct",
    v: order.total,
    id: paymentIntent.id,
  }),
});

Limits and honesty

Order values are clamped to a sane range on our side, so a typo cannot book an absurd figure that then has to be unpicked by hand.

Conversions count against the same monthly collection cap as pageviews. The cap exists so the service cannot run up a surprise bill; when it is reached we stop recording rather than keep spending, and say so with a 429.

We store the counters and nothing else. No customer identity, no email, no order contents, no IP address. The only fields that reach us are the six above.

Was this page helpful?