Server-to-Server Integration

Send events and purchases from your server or native app to Adverfly — no browser pixel required.

Endpoint

POST https://b.adverfly.com/e
Content-Type: application/json

The same endpoint the browser pixel uses. One event per request, sent in real time: events are timestamped when they arrive, so a batch sent hours later lands on the wrong day and outside its session.

Authentication

Create a server-side key in Adverfly under Data Exports → Server-side keys and send it with every S2S request:

X-Adverfly-Key: adv_s2s_…
  • Once your workspace has a key, every request with is_s2s: true without a valid key is rejected with 401. Without any key, S2S requests are accepted as before.
  • The key is shown once when you create it; Adverfly stores only a SHA-256 hash of it. Authorization: Bearer <key> is accepted as well.
  • Rotation: two keys can be active at once. Create the new key, switch your server over, then revoke the old one. Changes take effect within a minute.
  • Keep the key on your server only — never in an app bundle or a website. The browser pixel uses the same endpoint without a key, so the key authenticates your server-side events, not every event of the workspace.

Required Fields

FieldTypeDescription
store_idnumberYour Adverfly workspace ID
event_typestring"event" for visits and funnel steps (pageview, add_to_cart, initiated_checkout), "conversion" for end goals (purchase, lead)
namestringEvent name, e.g. "pageview", "add_to_cart", "purchase"
is_s2sbooleanAlways true for server-to-server requests
ipstringThe end customer's IP address, as its own top-level field. Never your server's IP
⚠ip — requests with is_s2s: true and no ip are rejected. The IP is used for visitor matching and forwarded to ad platforms (e.g. Meta CAPI) as the customer's IP. Do not put it into properties.

Recommended Fields

FieldTypeDescription
keysstring[]Stable visitor identifiers (see Keys). Without keys, visitors can only be matched by IP
parametersstringThe landing query string of the visit, e.g. "?utm_source=facebook&fbclid=…". This is what assigns the channel and campaign. Send it on the first event of a visit (web: the landing pageview; app: the app open / deep-link event)
propertiesobjectClick IDs (see Click IDs) and any custom fields you want to filter or break down by
hostnamestringA fixed host for this source, e.g. "www.myshop.com" or "app.myshop.com" for an app. Tell us a new host so your reports include it
pathnamestringPage or screen path, e.g. "/products/sneakers"
referrerstringReferring URL, if known
store_currencystringStore currency, e.g. "EUR"
store_timezonestringStore timezone, e.g. "Europe/Berlin"
ℹUser agent — read from the request's User-Agent header, not from the body. Forward the customer's device or browser user agent in that header, otherwise your server's is stored and forwarded.
ℹMissing values — omit the field entirely. No empty strings, no null, no placeholders.

Keys — Visitor Identification

An array of plain-text strings. Send every stable identifier you have; several of the same kind are fine. Adverfly hashes each key (SHA-256) before storage.

SourceRecommended keys
Web (Shopify)Shopify client ID (event.clientId), _ga cookie, _fbp cookie
Native appA stable install / visitor ID on every event, plus the shop's customer ID once the user is logged in (a Shopify customer GID is fine)
⚠Start every key with a short namespace such as app: or cus:. The first 4 characters of a key are stored readable (the rest is hashed), so never start a key with personal data such as an email address.
  • Stitching is automatic: when one request carries a key we already know, every other key in that request is linked to the same visitor. After login, send the install ID and the customer ID together once.
  • A key stays linked to its visitor for 28 days, and the link is reset after a purchase — the same as for the browser pixel.

Click IDs

Send the click IDs you have as properties on every event. They are forwarded to the ad platforms from there.

{
  "properties": {
    "_fbc": "fb.1.1727600000000.IwAR2xYz...",
    "_fbp": "fb.1.1727600000000.123456789",
    "gclid": "Cj0KCQjw9...",
    "gbraid": "0AAAAADx...",
    "wbraid": "CjkKEQjw...",
    "ttclid": "E.C.P.CqgBkTg...",
    "msclkid": "1a2b3c4d5e6f7a8b9c0d...",
    "sclid": "7f3e2a1b-...",
    "epik": "dj0yJnU9...",
    "tblci": "GiDk3m...",
    "obclid": "00a1b2c3..."
  }
}
KeyPlatform
_fbcMeta click ID, format fb.1.<timestamp_ms>.<fbclid>
_fbpMeta browser ID
gclid / gbraid / wbraidGoogle Ads
ttclidTikTok
msclkidMicrosoft Ads
sclidSnapchat
epikPinterest
tblciTaboola
obclidOutbrain
⚠Meta needs _fbc, not a bare fbclid. Build it as fb.1.<timestamp_ms>.<fbclid> from the fbclid of the landing URL. A bare fbclid in properties is stored but not forwarded to Meta. The raw query string still belongs in parameters for the channel assignment.
ℹThe older entry_meta.key / entry_meta.value arrays are still accepted and merged into properties on our side. Use one format, not both.

Purchases

FieldTypeDescription
transaction_idstringRequired. The order ID exactly as your shop sync delivers it to Adverfly. Shopify: the numeric order ID as a string, e.g. "6123456789012" — not the GID, not the order name. Plain text recommended (see Hashing)
transaction_currencystringRequired. Order currency (converted to the store currency)
transaction_gross_revenuenumberRecommended. Order total in cents (€99.00 = 9900). Your best estimate is fine
customer_idstringRecommended. The customer's email (hashed server-side). Drives new / returning detection and LTV
is_new_customernumberOptional. 1 = new, 0 = returning. Detected automatically when omitted
transaction_shipping_costs, transaction_taxnumberOptional, in cents
transaction_discount_code, transaction_city, transaction_country_codestringOptional
transaction_itemsarrayOptional line items (see the example below)
  • Matching with your order sync: the purchase event is joined to the synced order on transaction_id. The synced order provides the money (totals, shipping, tax, refunds), the event provides the attribution. That only works when both carry the same ID.
  • One purchase per order: a second purchase with an ID we already have is answered with 400 Transaction ID already exists — for example when the web checkout pixel already reported that order. Treat it as success and do not retry.
  • A purchase without transaction_id gets a random ID and can never be matched to an order.

Example: app open / landing ("pageview")

{
  "store_id": 123456,
  "is_s2s": true,
  "ip": "185.32.100.42",
  "event_type": "event",
  "name": "pageview",
  "hostname": "app.myshop.com",
  "pathname": "/products/sneakers",
  "store_currency": "EUR",
  "store_timezone": "Europe/Berlin",
  "keys": [
    "app:3f9c2e1a-7b4d-4c8e-9a1f-2d6b8e0c4a7f",
    "cus:gid://shopify/Customer/7234567890123"
  ],
  "parameters": "?utm_source=facebook&utm_campaign=autumn&fbclid=IwAR2xYz...",
  "properties": {
    "_fbc": "fb.1.1727600000000.IwAR2xYz...",
    "platform": "ios",
    "app_version": "3.4.0"
  }
}

Example: "add_to_cart"

{
  "store_id": 123456,
  "is_s2s": true,
  "ip": "185.32.100.42",
  "event_type": "event",
  "name": "add_to_cart",
  "hostname": "app.myshop.com",
  "pathname": "/products/sneakers",
  "store_currency": "EUR",
  "store_timezone": "Europe/Berlin",
  "keys": [
    "app:3f9c2e1a-7b4d-4c8e-9a1f-2d6b8e0c4a7f"
  ],
  "properties": {
    "_fbc": "fb.1.1727600000000.IwAR2xYz..."
  }
}

Example: "purchase"

{
  "store_id": 123456,
  "is_s2s": true,
  "ip": "185.32.100.42",
  "event_type": "conversion",
  "name": "purchase",
  "hostname": "app.myshop.com",
  "pathname": "/checkout/thank-you",
  "store_currency": "EUR",
  "store_timezone": "Europe/Berlin",
  "keys": [
    "app:3f9c2e1a-7b4d-4c8e-9a1f-2d6b8e0c4a7f",
    "cus:gid://shopify/Customer/7234567890123"
  ],
  "transaction_id": "6123456789012",
  "transaction_currency": "EUR",
  "transaction_gross_revenue": 9900,
  "customer_id": "customer@email.com",
  "transaction_items": [
    {
      "transaction_item_id": "SKU-001",
      "transaction_item_name": "Sneaker",
      "transaction_item_price": 4950,
      "transaction_item_quantity": 2
    }
  ],
  "properties": {
    "_fbc": "fb.1.1727600000000.IwAR2xYz..."
  }
}

Hashing

FieldWhat happens
keysHashed server-side (SHA-256); the first 4 characters stay readable. Send plain text
customer_idLower-cased, whitespace removed, then hashed (SHA-256). Plain text, or a SHA-256 hex of that normalised value
transaction_idStored as sent. Plain text is recommended: only a plain ID picks up the order totals from your shop sync and is deduplicated against the browser pixel. A SHA-256 hex of the plain ID is accepted and still recognised as the same order, but the event's own revenue is used
ipUsed for matching and forwarded to ad platforms as the customer's IP

Responses and Retries

StatusMeaningWhat to do
200Event accepted—
400 Transaction ID already existsThis order was already reportedTreat as success, do not retry
400 (any other message)Validation error, e.g. a missing required fieldFix the payload; retrying the same request will fail again
401S2S key missing or invalidSend a valid key in X-Adverfly-Key; do not retry unchanged
429 / 502 / 503 / 504Rejected before processingRetry with backoff
500 / timeoutMay already have been storedDo not retry events — only purchases are deduplicated (by transaction_id), every other event would be counted twice

Consent

Adverfly does not check consent server-side; sending an event means you have the legal basis for it. Apply the same rule as for your website tracking. Click IDs and anything that is forwarded to ad platforms typically require marketing consent.

Need help? Contact us at team@adverfly.com