# Send 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.

Canonical: https://revnu.com/docs/event-api
Last updated: 2026-10-18

The [Revnu Tag](https://revnu.com/docs/revnu-tag.md) 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.

```bash
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](https://revnu.com/docs/set-up-outcomes.md).
- **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](https://revnu.com/docs/revnu-tag.md).

## 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](https://revnu.com/docs/authentication.md).

## 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:

```js
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`:

```js
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:

   ```js
   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:

   ```ts
   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

```ts
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

```json
{
  "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](https://revnu.com/docs/set-up-outcomes.md). |
| `"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.
