Skip to Content
Browse the Docs▾

Send Events

POST/api/measurement/events

Tell Revnu from your server when a visitor signs up, books a call or pays, so the result is tied back to where they came from.

The Revnu Tag sees visits. The Event API is how your server tells Revnu something real happened: a signup, a booked call, a payment. Revnu joins the event to the visit that led to it, so you can see which source (an AI assistant, search, an ad) brought the customers who converted.

curl https://revnu.com/api/measurement/events \
  -H "Authorization: Bearer $REVNU_EVENT_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "event_id": "signup_8412",
    "event_name": "signup",
    "anonymous_id": "ra_3f9c0a1b2c3d4e5f60718293a4b5c6d7",
    "consent_state": "granted",
    "email": "ana@example.com"
  }'

No server of your own?

The Event API needs code that runs somewhere other than the visitor's browser. That can be small:

  • A serverless function on a host such as Vercel, Netlify or Cloudflare. Your site can stay on a no-code builder.

  • Your scheduler's webhook. Cal.com or a paid Calendly plan sends each booking to that function, and the function sends a booking event here. See set up outcomes.

  • Your sign-up or payment provider's webhook. Most auth and payment services can call a URL when an account is created or a payment succeeds. Point it at your function and send the event from there.

Revnu has no address a scheduler or provider can call directly. Without something of yours in between, sign-ups, bookings and payments can't be confirmed, and Revnu only sees visits through the Revnu Tag.

Your event key

The event key starts with rsk_live_. You get it in Ads → Revnu Tag, inside the setup prompt or under Install Details and Keys. Keep it in a server-side environment variable named REVNU_EVENT_KEY: anyone who has it can send events for your site. On Vercel and similar hosts, add it in the project's environment variables, not a browser-exposed one such as NEXT_PUBLIC_..., then redeploy. It is not the rvp_ personal API token.

Connect the visit to the event

The tag gives every visitor an id. Read it in the browser and send it to your backend with the signup, booking or checkout:

const anonymousId = window.revnu?.anonymousId; // "ra_..."

Then pass it as anonymous_id when your server sends the event. That id is what ties the event to the visit.

anonymousId is undefined while the visitor's consent is pending or denied: the tag doesn't start until consent is granted. Send the event anyway, without anonymous_id. It's still recorded, and can be matched from a stored ad click id.

Save the visitor's consent choice at the same moment, and send it as consent_state:

const consentState = window.revnu?.getContext().consentState; // "granted", "pending" or "denied"

Revnu sends every event to your ad platforms unless consent_state is denied, so an event without it still counts for your ads. Sending the saved choice is what keeps a visitor who declined out of them.

Report a sign-up from your backend

Tip

Repo connected? Ask Revnu to open the pull request that adds this call. With your site's GitHub repo connected in Connections → Learn Your Business → GitHub, say "open a PR that reports sign-ups to Revnu from our backend". Revnu opens it on a branch, never on main, and you review and merge it. Then add REVNU_EVENT_KEY to your host and redeploy.

Send the event from the code that creates the account, once the account exists, never from the browser or a thank-you page.

  1. In the browser, at sign-up, read the visitor id and consent and send both to your backend with the sign-up form:

    const ctx = window.revnu?.getContext();
    const anonymousId = window.revnu?.anonymousId; // "ra_..."
    const consentState = ctx?.consentState; // "granted", "pending" or "denied"
  2. On your server, save consentState with the new user, then send the event with an event_id built from your own user id, so a retry can never count twice:

    await fetch("https://revnu.com/api/measurement/events", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.REVNU_EVENT_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        event_id: `signup_${user.id}`,
        event_name: "signup",
        anonymous_id: anonymousId,
        consent_state: consentState,
        email: user.email,
      }),
    });
  3. Check the response. counted: false with a not_counted_reason means your outcome definition doesn't include sign-ups yet. To make sign-ups count, ask Revnu to track them as an outcome; it drafts the change and you approve it in Ads → Revnu Tag.

If the visitor had no anonymous_id (the tag was blocked, or they signed up on another device), still send the event. Revnu records it, and can match it from a stored ad click id if you pass one.

Sign-ups inside a desktop app. If people download an app from your site and sign up inside it, the tag never sees that sign-up, so these often arrive with no source. To keep the source, carry the id through yourself: add anonymous_id (or the ad click id from the landing page URL) to the download link or the sign-in redirect, have the app pass it to your server, and send it with the event. Without that, Revnu records the sign-up but can't say which ad or search brought it.

How dedup works

  • The same event_id again is a duplicate: the response says "duplicate": true and nothing is counted twice. Retry failed requests with the same event_id and body.

  • The same event_id with different values is refused with 409, so a bug can't overwrite a real event.

  • Pick ids from your own records: signup_<userId>, order_<orderId>, the scheduler's booking id. Never a random id per request, which defeats dedup.

  • A refund or cancellation is a new event with its own event_id and reversal_of set to the original one.

Request

Field Required Notes
event_id yes Your stable id for this event. 1-128 letters, digits, ., _, : or -. Sending the same id again is safe: it is reported as a duplicate, not counted twice.
event_name yes Lowercase snake_case, for example signup, demo_booked, purchase.
occurred_at no ISO 8601 date or milliseconds. Defaults to when Revnu receives it.
anonymous_id no window.revnu.anonymousId from the visit.
consent_state no granted, denied or pending: the visitor's choice saved with the signup, booking or checkout. Every event except denied is sent to your ad platforms.
google_ad_user_data, google_ad_personalization no granted, denied or unknown. Google Ads receives every event except one where google_ad_user_data is denied.
gclid, gbraid, wbraid, fbclid, ttclid, _ttp, twclid, li_fat_id, msclkid, rdt_cid no Ad click ids or Pixel cookies you stored yourself. Only needed when the visit cannot be joined by anonymous_id, for example when the tag was blocked.
email, phone, external_user_id no Normalized and hashed for matching.
amount, currency no Send both, for example 49 and "USD".
properties no An object of extra facts, for example { "plan": "pro" }.
reversal_of no The event_id of an earlier event this one cancels, such as a refund. Repeat the original amount and currency.

Node.js

await fetch("https://revnu.com/api/measurement/events", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.REVNU_EVENT_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    event_id: `signup_${user.id}`,
    event_name: "signup",
    occurred_at: user.createdAt.toISOString(),
    anonymous_id: revnuAnonymousId, // sent up from the browser
    consent_state: savedConsentState, // getContext().consentState, saved at signup
    email: user.email,
  }),
});

Response

{
  "accepted": true,
  "matched": false,
  "counted": false,
  "not_counted_reason": "\"signup\" is not in this site's win definition, which counts \"trial_started\". It was recorded, but it will not count as a win until the definition in Revnu includes it.",
  "duplicate": false,
  "enriched": false,
  "event_id": "signup_8412",
  "event_name": "signup",
  "roles": [],
  "attribution_status": "pending"
}
  • counted says whether the event counts as a win under your site's win definition.

  • not_counted_reason appears when an event can never count because the definition does not include its event_name. Change the definition in Revnu, or send the event it names.

  • matched says whether the event was credited to one of your Revnu ad campaigns. It stays false until you run campaigns through Revnu, and that is expected.

  • qualification_status appears when your definition has requirements, such as a work email or company size: qualified, not_qualified, or pending_evidence while Revnu still researches the company. qualification_missing names what's missing.

Code Meaning
200 Recorded and credited to one of your Revnu ad campaigns, or a duplicate of an event already recorded
202 Recorded, not credited to a campaign. Check counted to see whether it counts as a win
401 Missing or invalid event key
409 The same event_id was sent before with different values
413 Body over 64 KB
422 A field failed validation; error says which
429 Rate limited; retry later with the same event_id

Why an event didn't count

not_counted_reason starts with What it means Fix
This site has no win definition yet No outcome is set up, so nothing counts. Set one up in Ads → Revnu Tag. See set up outcomes.
"signup" is not in this site's win definition (with your event name) The event arrived, but your outcomes count other event names. The message lists them. Send the event name it lists, or ask Revnu to add this one as an outcome and approve the change in Ads → Revnu Tag.

counted: false with no not_counted_reason means the event name is in your definition but this one didn't meet its requirements, or Revnu is still researching the company. Check qualification_status and qualification_missing. A refund or cancellation sent with reversal_of never counts as a win itself.

Frequently Asked Questions

Can I send events from the browser?

Send events from your backend, a serverless function or a webhook handler, so your event key stays private. The browser passes along only the anonymous_id.

What happens if I send the same event twice?

Nothing is double counted. The response says "duplicate": true. Retrying a failed request with the same event_id and body is always safe.

Should I send page views?

The Revnu Tag already records visits for you. Send the outcomes your server knows are real: signups, bookings, trials, payments.

How do I send a sign-up from my backend so Revnu credits the source?

Read window.revnu.anonymousId and the consent state in the browser at sign-up, pass them to your server, and send a signup event from the code that creates the account, with event_id set to signup_ plus your user id and the REVNU_EVENT_KEY in the Authorization header.

Why is window.revnu.anonymousId undefined?

The visitor hasn't granted consent yet, or declined it, so the tag hasn't started. Send the event without anonymous_id; it is still recorded.

Can Revnu add the sign-up call to my code?

Yes, if your repo is connected on GitHub. Ask Revnu to open the pull request that reports sign-ups from your backend. You review and merge it, then add REVNU_EVENT_KEY to your host and redeploy.

Why do sign-ups from my desktop app have no source?

The Revnu Tag credits sources on your website. For your desktop app, pass anonymous_id or the ad click id through the download link or sign-in redirect to your server and send it with the event, and the sign-up carries its source.

I have no backend. Can I still send events?

Yes, with a small serverless function. Point your scheduler's or sign-up provider's webhook at it and send the event from there, and Revnu confirms sign-ups, bookings and payments from those events.

Why does my sign-up event say counted false?

Your outcome definition doesn't include sign-ups. The event is recorded, but it won't count as a result until the definition does. Ask Revnu to track sign-ups as an outcome and approve the change in Ads, Revnu Tag.

Where do I put REVNU_EVENT_KEY on Vercel?

In the project's server environment variables, under a name without a NEXT_PUBLIC_ prefix so it never reaches the browser. Redeploy afterwards so the server reads it.