# Server-side DataFast goals from Next.js, and why the Goals API returns 404

> Send goals from route handlers and server actions with POST /api/v1/goals, after the response. The visitor id comes from the request cookie, and it must already have a page view.

The sign-up form fires `signup_completed` in the browser. DataFast counts 180
this month. Your database has 231 new accounts. The missing 51 had blockers,
closed the tab on the redirect, or lost signal.

Anything you would investigate if it were wrong belongs on the server.

## The API

```
POST https://datafa.st/api/v1/goals
Authorization: Bearer df_...
Content-Type: application/json

{
  "datafast_visitor_id": "a3ab2331-989f-4cfa-91c6-2461c9e3c6bd",
  "name": "signup_completed",
  "metadata": { "method": "email" }
}
```

- The key is a website API key, `df_`, from Website settings, API. Server only.
  Never `NEXT_PUBLIC_`.
- `name` follows the same rules as browser goals: lowercase, digits, `_`, `-`,
  `:`, 64 characters. `identify` is reserved.
- `metadata`: at most 10 keys matching `^[a-z0-9_-]+$`, values up to 255
  characters.

## Where the visitor id comes from

A goal belongs to a visitor. The browser has the id in the
`datafast_visitor_id` cookie, and that cookie is first-party, so it arrives on
every request to your server. Read it in the server action:

```ts
import { cookies } from "next/headers";

const visitorId = (await cookies()).get("datafast_visitor_id")?.value;
```

From a webhook or a background job there is no browser cookie. Store the
visitor id on the user row at signup and read it from there.

## Why it 404s

`404` means "this visitor has no page views on this website". Common causes:

- **Local development.** The script skips localhost, so no page view was ever
  recorded for your dev visitor. Every server goal from dev 404s.
- **Consent not given.** The script never ran, so the cookie is stale or absent.
- **Wrong site.** The `df_` key belongs to a different website than the id.

Treat 404 as a log line, never as a failed request.

## Send it after the response

The goal must never slow a sign-up down, and on serverless an un-awaited
`fetch` can be frozen mid-flight when the response is sent. `after()` from
`next/server` fixes both: it runs once the response is out, and on Vercel it
keeps the function alive until the promise settles.

```ts
import { cookies } from "next/headers";
import { after } from "next/server";

export function trackGoalServer(name: string, metadata?: Record<string, string>): void {
  // Read the cookie now. Inside after() the request is gone.
  const pending = cookies().then((jar) => jar.get("datafast_visitor_id")?.value);

  after(async () => {
    const visitorId = await pending;
    const key = process.env.DATAFAST_API_KEY;
    if (!visitorId || !key) return;

    const response = await fetch("https://datafa.st/api/v1/goals", {
      method: "POST",
      headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
      body: JSON.stringify({ datafast_visitor_id: visitorId, name, metadata }),
      signal: AbortSignal.timeout(5_000),
    }).catch(() => null);

    if (response && !response.ok) {
      console.warn(`[analytics] goal ${name} answered ${response.status}`);
    }
  });
}
```

It returns `void` on purpose. Nobody can `await` it in the request path.

Two traps:

- **`after()` throws outside a request.** In a script or a cron job, catch that
  and send inline.
- **Fire after the write succeeds.** A goal for an account that failed to save
  is a lie in your funnel.

## Server or browser, never both

Pick one side per goal name. `checkout_started` in the browser is intent.
`signup_completed` on the server is a fact. Sending the same name from both
doubles the count, with no way to tell the copies apart.

## Check it

```bash
curl -X POST https://datafa.st/api/v1/goals \
  -H "Authorization: Bearer $DATAFAST_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"datafast_visitor_id":"<id from your cookie>","name":"signup_completed"}'
```

`200` with an `eventId` means the key, the visitor and the name are all good.

---

Agentic Boilerplate: A Next.js repo your agent already knows. Free during launch, then $99 once.

- Site map for agents: https://agenticboilerplate.com/llms.txt
- Public API: https://agenticboilerplate.com/openapi.json
- Contact: agenticstudio@gmail.com
