Order API and refunds
Use Settings → Integrations → Revenue, then expand Custom server integration (Order API) on your own website.
Create a server key
Choose Create revenue key. Copy the secret once and save it in your server environment as SEEAN_REVENUE_SECRET. Use the endpoint displayed under Integration examples. The site ID in the payload is the website’s internal ID, not its tracking token.
Send Authorization: Bearer <SEEAN_REVENUE_SECRET> and Content-Type: application/json. Never put this key in browser code.
Reporting currency
Choose the website’s currency from the dropdown below Reporting currency in the same card; the default is USD and the selection saves automatically. If saving fails, an error appears and the saved currency remains selected. Any ISO 4217 code from the list is accepted. Amounts are integers in that currency’s minor units: 2900 is USD 29.00, JPY 2,900 (no minor unit) or KWD 2.900 (three decimals).
Every order must use the reporting currency. A different code is rejected with HTTP 400 and {"result":"Order currency EUR does not match the website's reporting currency USD"} (with your codes); a code outside the list is rejected with HTTP 400 and {"error":"Unsupported currency code; use an ISO 4217 code such as USD"}. Changing the reporting currency does not convert orders already recorded; they are counted as excluded in analyses afterwards. The demo website’s currency cannot be changed.
Order payload
After verifying payment with your provider, send:
{
"siteId": "YOUR_SITE_ID",
"type": "order",
"externalId": "order-123",
"amount": 2900,
"currency": "USD",
"product": "Your product",
"kind": "one-time",
"visitorId": "VISITOR_ID_FROM_window.seean.attribution()",
"landingPath": "/pricing",
"convertingPath": "/checkout"
}
Subscription orders can use "kind": "subscription". Use a stable external order identifier. Optional fields: ts, visitorId, landingPath, convertingPath, campaign and path. visitorId links the order to the buyer’s tracked visits; touches, first source, last source, landing page and time to buy are then derived on the server. firstSource, lastSource, touches and timeToBuyMs are still accepted and are used only when the visitor has no tracked visit in the 90 days before the order. A supplied campaign always wins over the derived one.
Server request example
Save the payload above as order.json, replacing the site ID and order details with your verified payment data. In your server environment, set SEEAN_ORDER_ENDPOINT to the exact endpoint displayed in Settings and keep SEEAN_REVENUE_SECRET private. Then send:
curl --fail-with-body --request POST "$SEEAN_ORDER_ENDPOINT" \
--header "Authorization: Bearer $SEEAN_REVENUE_SECRET" \
--header "Content-Type: application/json" \
--data-binary @order.json
This creates a real order in the selected website. Use a separate test website when checking your integration. A new accepted order returns HTTP 201 with {"result":"created"}. Check Revenue in a range containing its timestamp. Sending the identical file again returns 200 with {"result":"duplicate"} and does not create a second order.
The payment amount, currency, order ID and payment status must come from your verified server-side payment record. Browser-supplied checkout values are not proof of payment. Capture window.seean.attribution() with the checkout request, validate the supported fields on your server, and associate them with the verified order before forwarding them. Missing attribution does not prevent order ingestion; an order without visitorId is recorded as Unattributed. See the attribution handoff for that separate step.
Field and timestamp limits
The request body is limited to 8 KiB. Required fields are siteId, type, externalId, amount, currency, product and kind for orders. Unknown fields are rejected. externalId and product accept up to 128 characters. amount must be an integer from 0 through 100,000,000,000 minor units, and kind must be one-time or subscription. visitorId accepts up to 64 characters; landingPath and convertingPath up to 300 characters starting with /; firstSource and lastSource up to 128; campaign up to 64; path up to 1,000.
Optional ts is a Unix timestamp in milliseconds. If omitted, the order uses its first successful ingestion time. A new timestamp must be within the preceding 1,098 days, with at most one minute of future clock tolerance. Keep the original timestamp and all other fields unchanged on retries. For subscriptions, use a different external ID for each successful renewal payment; reusing the subscription ID would deduplicate or conflict with later payments.
After Reset statistics, a new order whose effective timestamp is earlier than the reset is rejected with 400 Order predates the statistics reset, without consuming order admission quota or recreating the removed record. The exact reset timestamp is accepted. Existing identical preserved orders still return duplicate; reset does not authorize a replay under a different ID.
A new direct pi_… order is rejected with 409 Payment reference already belongs to a preserved order when that payment already has an initial-payment link on this website. The check also applies without retained Stripe read access. Keep the original order ID; neither its amount nor its payment links are overwritten.
Refunds
Send to the same endpoint:
{
"siteId": "YOUR_SITE_ID",
"type": "refund",
"externalId": "order-123",
"amount": 900,
"refundId": "re_123",
"ts": 1757600000000
}
amount(optional) is the refunded amount in minor units. Omit it to refund the full remaining amount.0is rejected.refundId(optional, up to 128 characters) is your provider’s refund identifier. A refund with arefundIdalready recorded on that order returns 200{"result":"duplicate"}and changes nothing, so retries are safe. Without arefundId, a refund on an already fully refunded order returnsduplicate; a partial refund without one is applied again on each request.ts(optional) is the refund time in milliseconds, within the preceding 1,098 days; it defaults to the ingestion time.
A refund must refer to an already accepted order (404 otherwise). A successful refund returns {"result":"refunded"}. An amount larger than the remaining unrefunded amount returns 400 {"result":"Refund exceeds the remaining order amount"}; the 51st refund on an order returns 400 {"result":"Maximum 50 refunds per order"}. When the refunded total reaches the order amount the order is marked refunded.
Refunds reduce net revenue in the period of the original order, not the period of the refund date. Review the range containing the order; no separate refund-date transaction is added.
For Stripe subscription Checkout orders whose stored identifier is sub_…, keep that original value as externalId and use the successful Stripe refund’s re_… ID as refundId. Do not create a second pi_… order just to match a refund. Optional owner-consented Stripe read access can prove initial Checkout payment links while preserving existing IDs and fingerprints; it does not ingest renewals, start a backfill or change this API’s payload. Explicit single-session reconciliation is a separate authenticated owner action, not an endpoint of this Order API. Before sending refunds manually, verify they succeeded and check whether that same refund ID is already recorded; a different ID can deduct it again.
A newly applied refund invalidates older on-demand Overview breakdowns. Refresh the analysis to see matching totals and details; saved results are not silently rewritten. An idempotent duplicate refund does not invalidate them again.
Response handling
| HTTP status | Meaning and next action |
|---|---|
| 200 / 201 | Accepted or already recorded. Read the result value. |
| 400 | Invalid JSON, unsupported fields, currency, amount or timestamp; a currency other than the reporting currency; a refund above the remaining amount or beyond 50 per order. Correct the payload before retrying. |
| 401 | Missing, invalid or rotated secret, wrong website, or a website that cannot accept orders. Verify the server key and site ID. |
| 404 | A refund’s original order is missing. Confirm the order succeeded before retrying the refund. |
| 409 | The external ID belongs to different order data, or the payment reference already links to a preserved order. Resolve the mismatch; do not generate a new ID merely to bypass the conflict. |
| 413 | The body exceeds 8 KiB. Remove unsupported or unnecessary data. |
| 429 / 503 | Temporarily unable to accept the request. Retry later with the same payload and external ID. |
Validation and transport errors can use an error field; ingestion responses use result, including some non-success responses. Decide success from the HTTP status, not merely the presence of result. Preserve failed jobs in your server’s existing delivery process so a timeout or browser closure cannot silently lose a verified payment.
Retries and key rotation
Tracked attribution uses the current analytics store. Previously accepted orders keep their original amounts and attribution when retried.
Orders with a visitor ID request an immediate transfer of queued visits and wait for accepted visits to become available before their attribution is captured. If that history cannot be checked, the request returns 503 without accepting a partial attribution; retry the same payload and external ID. Orders without a visitor ID and refunds do not need a visitor-history lookup. An already accepted identical order returns duplicate without reading the history again.
Identical orders with the same externalId, and refunds with the same refundId, are deduplicated. Conflicting order data returns 409. Retry 429 or 503 later with the same payload and identifier.
Rotating the revenue key invalidates the previous key immediately. Update the server integration before expecting later orders to arrive. This is a custom server integration; provider webhook verification remains your responsibility.
Large visitor histories
A successful order response confirms the payment record, not complete attribution. If its visitor history exceeds the attribution limits, the order remains in exact monetary totals and Revenue shows incomplete attribution. Retrying an identical order returns duplicate; it does not recompute that history.
Something unclear? Contact support.