# Send events from your server

Some things happen on your server, not on the phone: a subscription renews, a webhook arrives, a job finishes. Send them to Apparent over HTTP with the same batch the SDKs use. Nothing to install.

## The write key is safe on your server

The key (`k`) only lets you add events. It already ships inside your app, so it is not a secret, and keeping it in your server's settings is fine. Find it on the Connect page.

## Send an event

```js Node
import { randomUUID } from "node:crypto";

export async function track(userId, name, props = {}) {
  const now = Date.now();
  const res = await fetch("https://in.apparent.fyi/i", {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({
      a: process.env.APPARENT_APP,
      k: process.env.APPARENT_KEY,
      v: "server-0.1.0",
      sa: now,
      d: { platform: "ios", os: "18.0", app: "1.0.6", locale: "en-US",
           anon: userId, user: userId },
      e: [{ n: name, t: now, s: "server",
            i: randomUUID().replaceAll("-", ""), p: props }],
    }),
  });
  if (!res.ok) throw new Error(`Apparent answered ${res.status}`);
}
```

```python Python
import os, time, uuid, requests

def track(user_id, name, props=None):
    now = int(time.time() * 1000)
    r = requests.post("https://in.apparent.fyi/i", timeout=10, json={
        "a": os.environ["APPARENT_APP"],
        "k": os.environ["APPARENT_KEY"],
        "v": "server-0.1.0",
        "sa": now,
        "d": {"platform": "ios", "os": "18.0", "app": "1.0.6",
              "locale": "en-US", "anon": user_id, "user": user_id},
        "e": [{"n": name, "t": now, "s": "server",
               "i": uuid.uuid4().hex, "p": props or {}}],
    })
    r.raise_for_status()
```

```sh curl
curl https://in.apparent.fyi/i -H 'content-type: application/json' -d '{
  "a": "<app id>", "k": "<write key>", "v": "server-0.1.0",
  "sa": '"$(date +%s)000"',
  "d": { "platform": "ios", "os": "18.0", "app": "1.0.6",
         "locale": "en-US", "anon": "user_8812", "user": "user_8812" },
  "e": [{ "n": "subscription_renewed", "t": '"$(date +%s)000"',
          "s": "server", "i": "'"$(uuidgen | tr -d - | tr A-Z a-z)"'",
          "p": { "plan": "yearly" } }]
}'
```

A good answer is `{"ok":true,"accepted":1,...}`. `401` means the app id or key is wrong. On `408`, `429` or `5xx`, wait and send the same batch again (same `i`). On `401` or other `4xx`, stop.

## Who it was

- **A signed-in person:** put your own account id in `user` (or `u` on one event). Apparent counts everyone with the same user id as one person, across all their phones and reinstalls, so this event joins the same person as their app events. Use the same id you pass to `identify()` in the app.
- **Send the user id as `anon` too.** People are found through `anon`, so one shared value for every user (like `"server"`) would merge them all into the first person. With `anon` equal to `user` the event lands on that person, and on the same person as their phone events.
- An event with no user id counts as its own person, one per `anon`.
- A server event is not someone opening the app, so it doesn't make a person "active" on its own. Their app visits do.

## The event id

`i` is a random id for each event (32 hex characters, up to 80). Send the same one when you retry: the server keeps one copy per id, so sending twice is safe. For a webhook, a hash of the webhook's own id works well, so a repeated delivery never counts twice.

All fields and limits are in the [Intake API](https://apparent.fyi/docs/api).
