Send signup and order events from your app to your own dashboard with curl

By

Post signup and order events with curl to Events Logger, a self-hosted SQLite dashboard, and read them back in the project feed.

Self-hosted event tracking keeps a signup or an order in a database you run, on a machine you control.

Events Logger is that dashboard. Your app sends one JSON event over HTTP, and the event shows up in a feed. SQLite stores it in a local file. You don’t create an account for the dashboard, and it doesn’t ask for a cloud API key.

First-party analytics with SQLite is the pageview side of this idea: one events table, funnel queries, beacons from the browser. This tutorial is the other kind of event, the ones your own backend already knows about. A person registered. An order was placed.

A webhook is a POST some other service sends you when something happens, such as a payment. The call below is the same shape of request, sent by your app to a dashboard you host.

What you need

Events Logger runs on Node.js 18 or later, with npm. After purchasing it, download the source zip from your Polar receipt and unzip it.

Keep the installation on your machine, or behind an authenticated private gateway. The shipped dashboard has no login. It shows the global API key on the home page, and its reading and dashboard mutation routes are unauthenticated. The API expects that key when you create a project, push an event, or update an insight card. Don’t put this server on the public internet as it ships.

The examples use a made-up shop, Garden Shop, with a placeholder key and project id. Copy your own from the dashboard, and don’t commit the real key.

Run the dashboard and create a project

From the unzipped folder, install dependencies and start the server:

npm install
npm run dev

Open http://localhost:4321. On first startup the app creates data/events.db and its tables, and it generates one global API key. That key is on the home page. You’ll send it as a Bearer token.

Create a project named Garden Shop from the home page. Each project has its own feed, charts, insights, and settings.

Open that project’s Playground. It shows the exact HTTP call for this installation, which is where you read the project id. The id in the samples below, q4q8nb18qc2i, is a placeholder, and yours will be different. New categories are created as events arrive, so you don’t define orders or signups ahead of time.

Send a signup and an order with curl

The base URL is http://localhost:4321 when you run it locally. Change it if you later put the dashboard on a private server.

A signup only needs the three required fields, project, category, and title:

curl --fail-with-body -X POST http://localhost:4321/api/events \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ev_your-key-here" \
  -d '{
    "project": "q4q8nb18qc2i",
    "category": "signups",
    "title": "User Registered"
  }'

Replace ev_your-key-here with the key on the home page, and replace the project id with the one in the Playground. The API returns the stored event as JSON with status 201.

--fail-with-body keeps the error response visible and makes curl exit with an error on HTTP failures. Plain curl can print a 401 response and still exit successfully, which is easy to miss in a script.

An order can carry a description, where bold text and links work. Here Maria places order 1234:

curl --fail-with-body -X POST http://localhost:4321/api/events \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ev_your-key-here" \
  -d '{
    "project": "q4q8nb18qc2i",
    "category": "orders",
    "title": "Order Placed",
    "description": "Order **#1234** placed by **Maria**"
  }'

You should now have both events in the Garden Shop feed. The feed is chronological, and it refreshes while you leave it open. Filter by signups or orders, or search the title and the description.

Optional fields sit next to those three. tags is a set of key-value pairs you can search. url turns the title into a link, for example to that order in your own app. user_id attaches a user id. icon is an emoji. notify asks for a browser notification. Leave them out until you need them.

Send a signup or order to your own dashboardRun Events Logger, create a project, copy the API key and project id, and POST the event with curl. A 201 means the feed has it. A 401 means the key is wrong.Yes401npm run devCreate Garden ShopCopy key and projectidcurl POST/api/eventsStatus 201?Event in the feedCheck the API key

The same push exists as a CLI command, run from the unzipped folder:

node cli/index.js push \
  --api-key ev_your-key-here \
  --project q4q8nb18qc2i \
  --category signups \
  --title "User Registered"

The key is an argument on this command, so it can show up in a log or in a shared terminal recording. The backend example below reads the key from the environment instead. node cli/index.js --help lists the rest of the options.

Read the event back

You can fetch the events over HTTP too. Read-only calls don’t send the Bearer token:

curl "http://localhost:4321/api/events?project=q4q8nb18qc2i&category=orders&limit=20"

The response has events and a nextCursor for the next page. Point project at your real project id. category=orders keeps the signups out of this page. Events arrive newest first. If nextCursor is 48, add cursor=48 with the same filters to fetch older rows. A null cursor means there is no next page.

The search parameter matches titles, descriptions, and tags. With --get and --data-urlencode, curl builds the query string and encodes the values for you:

curl --fail-with-body --get http://localhost:4321/api/events \
  --data-urlencode 'project=q4q8nb18qc2i' \
  --data-urlencode 'category=orders' \
  --data-urlencode 'search=1001' \
  --data-urlencode 'limit=20'

It’s a text match, so 1001 can also match that number inside a longer description.

Charts on the project shows how often each category arrives over time.

Send the same event from your backend

Once the curl returns 201, the app should send the event itself. Copy the API key into the server environment as EVENTS_API_KEY, and the Garden Shop project id as EVENTS_PROJECT_ID. Keep both out of the browser bundle. This uses Node’s built-in fetch:

const response = await fetch('http://localhost:4321/api/events', {
  method: 'POST',
  signal: AbortSignal.timeout(5000),
  headers: {
    'Content-Type': 'application/json',
    Authorization: `Bearer ${process.env.EVENTS_API_KEY}`,
  },
  body: JSON.stringify({
    project: process.env.EVENTS_PROJECT_ID,
    category: 'orders',
    title: 'Order packed',
    tags: { order: '1001' },
  }),
})

if (!response.ok) throw new Error(`Event request failed: ${response.status}`)

Change the URL if the dashboard is on a private server. The source also includes examples for Python, PHP, Ruby, and Go.

Open the Garden Shop feed and you should see Order packed, with the order tag. Using the order number as a tag lets you find Order placed, Order packed and Order dispatched for the same order without putting a customer’s email address in every row.

Send the event after your application has saved that state. If the dashboard is down, the order is still packed, so catch the error where you call it and don’t roll back the business operation. The five-second timeout keeps a stopped dashboard from holding the request open. If events must survive a restart, save a pending delivery in your own database and let a worker send it later.

Be careful with retries. The ingestion endpoint doesn’t deduplicate repeated POSTs, and after a timeout the dashboard may have saved the row before the response was lost. Check the feed before retrying, or a retry can create a second event.

If something looks wrong

StatusWhat to check
400Missing project, category or title
401Bearer token does not match the installation’s key
404The project ID does not exist on this installation

A 401 means the token doesn’t match the global API key on the home page. Copy it again. The key is created the first time the app starts, and a new data/ directory gets its own key.

If the event lands in the wrong project, compare project in the JSON with the id shown in that project’s Playground.

If the feed stays empty while curl printed JSON, you are looking at another project, or the server you posted to is not the one in the browser. Use the URL Astro printed, and the project you created on that server.

If you later move the dashboard off your machine, build with npm run build. The standalone Node entry is dist/server/entry.mjs. A hosted copy needs HTTPS, a persistent data/ directory, and an authentication gateway in front of every dashboard and API route, because the API key only covers a few API calls.

Get Events Logger

Track signups, orders, deploys and errors in your own SQLite dashboard. Includes an HTTP API, CLI, charts and full MIT source.

See Events Logger → · Read the docs · $20 including tax, with full source code.

More tutorials for my apps →

Want me to talk about your product? You can sponsor this site.