Files
posthog.com/contents/tutorials/api-capture-events.mdx
Vincent (Wen Yu) Ge 9b76cc81da Update capture tutorial (#13335)
* Update capture tutorial

* batching base url

* Updated tutorial

* They should all be from ian, remove env. vars
2025-10-22 12:23:24 -04:00

706 lines
21 KiB
Plaintext

---
title: Using the PostHog API to capture events
date: 2025-10-22T00:00:00.000Z
author:
- ian-vanagas
showTitle: true
sidebar: Docs
tags:
- events
- persons
- product analytics
- product os
---
export const apiEventsLight = "https://res.cloudinary.com/dmukukwp6/image/upload/w_1600,c_limit,q_auto,f_auto/view_event_light_30a37c70a8.png"
export const apiEventsDark = "https://res.cloudinary.com/dmukukwp6/image/upload/w_1600,c_limit,q_auto,f_auto/view_event_dark_97fe1d29d5.png"
PostHog provides [libraries](/docs/integrate?tab=sdks) that make it easy to capture events in popular languages. These libraries are basically wrappers around the API. They handle and automate common tasks like capturing pageviews.
Using the API directly allows for any language that can send requests to capture events, or completely customize your implementation. Using the API to capture events directly also gives you a better understanding of [PostHog's event-based data structures](/docs/how-posthog-works/data-model) which is abstracted if you use a library.
## Base URL
The base URL of your PostHog depends on the region of your PostHog project:
<MultiLanguage>
```bash file=US
https://us.i.posthog.com
```
```bash file=EU
https://eu.i.posthog.com
```
</MultiLanguage>
## Capture endpoint
PostHog captures events through `/i/v0/e/` endpoint of your project region.
For your PostHog project (if you're authenticated on [PostHog](https://app.posthog.com/)), the fill URL is:
```bash
<ph_client_api_host>/i/v0/e/
```
You can also use the `/batch` endpoint to capture multiple events in one request. We cover this in the [batching events section](#batching-events).
```bash
<ph_client_api_host>/batch/
```
## Authenticating with the project API key
The first thing needed, like the [basic GET request tutorial](/tutorials/api-get-insights-persons), is to authenticate ourselves in the API. Unlike in the GET request tutorial, we can use the project API key (the same key you use to initialize a PostHog library). This can be found in your project settings.
The project API key is a write-only key, which works perfectly for the POST-only endpoints we want to access.
## Basic event capture request
To capture events, all we need is a project API key, the data we want to send, and a way to send a request. To capture a new event, you need to send a `POST` request to `<ph_client_api_host>/i/v0/e/` (or the `/i/v0/e` endpoint for your instance) with the project API key, event name, and distinct ID.
<MultiLanguage>
```bash
# curl
curl -v -L --header "Content-Type: application/json" -d '{
"api_key": "<ph_project_api_key>",
"event": "request",
"distinct_id": "ian@posthog.com"
}' <ph_client_api_host>/i/v0/e/
```
```python
import requests
headers = {
"Content-Type": "application/json",
}
body = {
"api_key": '<ph_project_api_key>',
"event": "request",
"properties": {
"distinct_id": "ian@posthog.com"
}
}
url = "<ph_client_api_host>/i/v0/e/"
response = requests.post(url, headers=headers, json=body)
print(response.json())
```
```js file=NodeJS
require('dotenv').config();
const headers = {
"Content-Type": "application/json",
};
const body = {
"api_key": "<ph_project_api_key>",
"event": "request",
"properties": {
"distinct_id": "ian@posthog.com"
}
};
const url = "<ph_client_api_host>/i/v0/e/";
fetch(url, {
method: 'POST',
headers: headers,
body: JSON.stringify(body)
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
```
</MultiLanguage>
Once you've done that, you should see the event in your PostHog project's [activity tab](https://app.posthog.com/activity/explore).
<ProductScreenshot
imageLight = {apiEventsLight}
imageDark = {apiEventsDark}
classes="rounded"
alt="Events"
/>
### Adding properties and batching
You can also add arbitrary properties and a timestamp in [ISO 8601 format](https://en.wikipedia.org/wiki/ISO_8601) to this request. If you don't add a timestamp, we automatically set it to the current time.
<MultiLanguage>
```bash
curl -v -L --header "Content-Type: application/json" -d '{
"api_key": "<ph_project_api_key>",
"properties": {
"request_size": "big",
"api_request": true
},
"timestamp": "2022-09-21 09:03:11.913767",
"distinct_id": "ian@posthog.com",
"event": "big_request"
}' <ph_client_api_host>/i/v0/e/
```
```python
import requests
headers = {
"Content-Type": "application/json",
}
body = {
"api_key": '<ph_project_api_key>',
"event": "big_request",
"timestamp": "2022-10-21 09:03:11.913767",
"properties": {
"distinct_id": "ian@posthog.com",
"request_size": "big",
"api_request": True
}
}
url = "<ph_client_api_host>/i/v0/e/"
response = requests.post(url, headers=headers, json=body)
print(response.json())
```
```js file=NodeJS
const headers = {
"Content-Type": "application/json",
};
const body = {
"api_key": "<ph_project_api_key>",
"event": "big_request",
"timestamp": "2022-10-21 09:03:11.913767",
"properties": {
"distinct_id": "ian@posthog.com",
"request_size": "big",
"api_request": true
}
};
const url = "<ph_client_api_host>/i/v0/e/";
fetch(url, {
method: 'POST',
headers: headers,
body: JSON.stringify(body)
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
```
</MultiLanguage>
You can also batch these requests together by sending a list of events to the `/batch/` endpoint. This is useful for limiting the number of requests you make. Events can be held, then sent as a batch. PostHog SDKs do this automatically, and we use batching to process events.
<MultiLanguage>
```bash
curl -v -L --header "Content-Type: application/json" -d '{
"api_key": "<ph_project_api_key>",
"batch": [
{
"event": "batched_event",
"properties" : {
"distinct_id": "ian@posthog.com",
"number_in_batch": 1
}
},
{
"event": "batched_event",
"properties" : {
"distinct_id": "ian@posthog.com",
"number_in_batch": 2
}
}
]
}' <ph_client_api_host>/batch/
```
```python
import requests
url = "<ph_client_api_host>/batch/"
headers = {
"Content-Type": "application/json",
}
body = {
"api_key": "<ph_project_api_key>",
"batch": [
{
"event": "batched_event",
"properties" : {
"distinct_id": "ian@posthog.com",
"number_in_batch": 1
}
},
{
"event": "batched_event",
"properties" : {
"distinct_id": "ian@posthog.com",
"number_in_batch": 2
}
}
]
}
response = requests.post(url, headers=headers, json=body)
print(response.json())
```
```js file=NodeJS
const headers = {
"Content-Type": "application/json",
};
const body = {
"api_key": "<ph_project_api_key>",
"batch": [
{
"event": "batched_event",
"properties" : {
"distinct_id": "ian@posthog.com",
"number_in_batch": 1
}
},
{
"event": "batched_event",
"properties" : {
"distinct_id": "ian@posthog.com",
"number_in_batch": 2
}
}
]
};
const url = `<ph_client_api_host>/batch/`;
fetch(url, {
method: 'POST',
headers: headers,
body: JSON.stringify(body)
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
```
</MultiLanguage>
## Identifying and aliasing users
You can also `POST` `$identify` events to add more details about those users. The API has no concept of state so the user information is not added as properties unless you send it in a request. It is not automatically created or included in the request like it is in the [JavaScript](/docs/integrate/client/js) library.
You still send identify events to the `/i/v0/e/` endpoint. Use `$set` to set the person properties you want.
<MultiLanguage>
```bash
curl -v -L --header "Content-Type: application/json" -d '{
"api_key": "<ph_project_api_key>",
"distinct_id": "ian@posthog.com",
"$set": {
"email": "ian@posthog.com",
"is_cool": true
},
"event": "$identify"
}' <ph_client_api_host>/i/v0/e/
```
```python
import requests
url = '<ph_client_api_host>/i/v0/e/'
headers = {
"Content-Type": "application/json",
}
body = {
"api_key": "<ph_project_api_key>",
"distinct_id": "ian@posthog.com",
"$set": {
"email": "ian@posthog.com",
"is_cool": False
},
"event": "$identify"
}
response = requests.post(url, headers=headers, json=body)
print(response.json())
```
```js file=NodeJS
const headers = {
"Content-Type": "application/json",
};
const body = {
"api_key": "<ph_project_api_key>",
"distinct_id": "ian@posthog.com",
"$set": {
"email": "ian@posthog.com",
"is_cool": true
},
"event": "$identify"
};
const url = "<ph_client_api_host>/i/v0/e/";
fetch(url, {
method: 'POST',
headers: headers,
body: JSON.stringify(body)
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
```
</MultiLanguage>
### Aliasing users
If you have two users you'd like to combine together, you can use a `$create_alias` event. See more about this in our [identifying users documentation](/docs/integrate/identifying-users).
<MultiLanguage>
```bash
curl -v -L --header "Content-Type: application/json" -d '{
"api_key": "<ph_project_api_key>",
"properties": {
"distinct_id": "ian@posthog.com",
"alias": "ian2@posthog.com"
},
"event": "$create_alias"
}' <ph_client_api_host>/i/v0/e/
```
```python
import requests
url = '<ph_client_api_host>/i/v0/e/'
headers = {
"Content-Type": "application/json",
}
body = {
"api_key": "<ph_project_api_key>",
"properties": {
"distinct_id": "ian@posthog.com",
"alias": "ian2@posthog.com"
},
"event": "$create_alias"
}
response = requests.post(url, headers=headers, json=body)
print(response.json())
```
```js file=NodeJS
const headers = {
"Content-Type": "application/json",
};
const body = {
"api_key": "<ph_project_api_key>",
"properties": {
"distinct_id": "ian@posthog.com",
"alias": "ian2@posthog.com"
},
"event": "$create_alias"
};
const url = "<ph_client_api_host>/i/v0/e/";
fetch(url, {
method: 'POST',
headers: headers,
body: JSON.stringify(body)
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
```
</MultiLanguage>
## Capturing errors
Everything is an event in PostHog. [Error tracking](/docs/error-tracking) is no different. You can manually capture errors by sending an `$exception` event with the following properties:
| Property | Description |
|----------|-------------|
| `$exception_list` | A list of exception objects with detailed information about each error. Each exception can include a `type`, `value`, `mechanism`, `module`, and a `stacktrace` with `frames` and `type`. You can find the expected schema as types for both [exception](https://github.com/PostHog/posthog/blob/master/rust/cymbal/src/types/mod.rs#L39) and [stack frames](https://github.com/PostHog/posthog/blob/master/rust/cymbal/src/langs/custom.rs#L12) in our Rust repo |
| `$exception_fingerprint` | (Optional) The identifier used to group issues. If not set, a unique hash based on the exception pattern will be generated during ingestion |
Here's an example of how to capture an error:
<MultiLanguage>
```bash
curl -X POST "<ph_client_api_host>/i/v0/e/" \
-H "Content-Type: application/json" \
-d '{
"api_key": "<ph_project_api_key>",
"event": "$exception",
"properties": {
"distinct_id": "ian@posthog.com",
"$exception_list": [{
"type": "ScriptError",
"value": "Command not found: fake_command",
"mechanism": {
"handled": true,
"synthetic": false
},
"stacktrace": {
"type": "raw",
"frames": [
{
"platform": "custom",
"lang": "bash",
"function": "main",
"filename": "basic-exception.sh",
"lineno": 15,
"colno": 1,
"module": "script_execution",
"resolved": true,
"in_app": true
},
{
"platform": "custom",
"lang": "bash",
"function": "execute_command",
"filename": "utils.sh",
"lineno": 42,
"colno": 5,
"module": "command_handler",
"resolved": true,
"in_app": true
},
{
"platform": "custom",
"lang": "bash",
"function": "error_event_bash",
"filename": "error_handler.sh",
"lineno": 8,
"colno": 12,
"module": "error_tracking",
"resolved": false,
"in_app": false
}
]
}
}],
"$exception_fingerprint": <MD5_HASH_OF_EXCEPTION_MESSAGE>
}
}'
```
```python
import requests
import os
import traceback
import hashlib
import time
url = "<ph_client_api_host>/i/v0/e/"
headers = {
"Content-Type": "application/json",
}
# Create a fake exception for demonstration
try:
# Simulate an error_event_python
raise ValueError("error_event_python: This is a simulated error for testing")
except Exception as e:
# Get the current traceback
tb = traceback.format_exc()
# Create exception fingerprint
fingerprint = hashlib.md5(str(e).encode()).hexdigest()
body = {
"api_key": "<ph_project_api_key>",
"event": "$exception",
"properties": {
"distinct_id": "ian@posthog.com",
"$exception_list": [{
"type": type(e).__name__,
"value": str(e),
"mechanism": {
"handled": True,
"synthetic": False
},
"stacktrace": {
"type": "raw",
"frames": [
{
"platform": "custom",
"lang": "python",
"function": "error_event_python",
"filename": "basic-exception.py",
"lineno": 15,
"colno": 1,
"module": "exception_handler",
"resolved": True,
"in_app": True
},
{
"platform": "custom",
"lang": "python",
"function": "simulate_error",
"filename": "error_simulator.py",
"lineno": 8,
"colno": 5,
"module": "testing",
"resolved": True,
"in_app": True
},
{
"platform": "custom",
"lang": "python",
"function": "main",
"filename": "app.py",
"lineno": 42,
"colno": 12,
"module": "application",
"resolved": False,
"in_app": False
}
]
}
}],
"$exception_fingerprint": fingerprint
}
}
response = requests.post(url, headers=headers, json=body)
print(response.json())
```
```js file=NodeJS
const crypto = require('crypto');
const headers = {
"Content-Type": "application/json",
};
// Create a fake exception for demonstration
try {
// Simulate an error_event_javascript
throw new Error('error_event_javascript: This is a simulated error for testing');
} catch (error) {
// Create exception fingerprint
const fingerprint = crypto.createHash('md5').update(error.message).digest('hex');
const body = {
"api_key": "<ph_project_api_key>",
"event": "$exception",
"properties": {
"distinct_id": "ian@posthog.com",
"$exception_list": [{
"type": error.name,
"value": error.message,
"mechanism": {
"handled": true,
"synthetic": false
},
"stacktrace": {
"type": "raw",
"frames": [
{
"platform": "custom",
"lang": "javascript",
"function": "error_event_javascript",
"filename": "basic-exception.js",
"lineno": 8,
"colno": 1,
"module": "exception_handler",
"resolved": true,
"in_app": true
},
{
"platform": "custom",
"lang": "javascript",
"function": "simulateError",
"filename": "error-simulator.js",
"lineno": 15,
"colno": 5,
"module": "testing",
"resolved": true,
"in_app": true
},
{
"platform": "custom",
"lang": "javascript",
"function": "main",
"filename": "app.js",
"lineno": 42,
"colno": 12,
"module": "application",
"resolved": false,
"in_app": false
}
]
}
}],
"$exception_fingerprint": fingerprint
}
};
const url = "<ph_client_api_host>/i/v0/e/";
fetch(url, {
method: 'POST',
headers: headers,
body: JSON.stringify(body)
})
.then(response => response.json())
.then(data => console.log(data))
.catch(err => console.error('Error:', err));
}
```
</MultiLanguage>
While possible, we strongly recommend you stick to using our [error tracking](/docs/error-tracking/installation) SDKs instead of manually capturing errors for features like accurate fingerprinting, source-map support, release tracking, and more.
## Capturing LLM analytics events
It's also possible to capture LLM analytics events using the API. If you're using a language without [SDK support for LLM analytics](/docs/llm-analytics/installation), you can use the API to capture events.
To capture LLM analytics, you need to capture 4 types of events:
| Event type | Event name | Documentation |
|--------------|-------------------|---------------------------------------------|
| Generations | `$ai_generation` | [What are generations?](/docs/llm-analytics/generations) |
| Spans | `$ai_span` | [What are spans?](/docs/llm-analytics/spans) |
| Traces | `$ai_trace` | [What are traces?](/docs/llm-analytics/traces) |
| Embeddings | `$ai_embedding` | [What are embeddings?](/docs/llm-analytics/embeddings) |
You can find more information in our [manual capture guide](/docs/llm-analytics/manual-capture).
## Further reading
- [How to use the PostHog API to get insights and persons](/tutorials/api-get-insights-persons)
- [Documentation on our event capture API endpoint](/docs/api/capture)
- [How to evaluate and update feature flags with the PostHog API](/tutorials/api-feature-flags)
- [Manual error tracking capture](/docs/error-tracking/installation/manual)
- [Manual LLM analytics capture](/docs/llm-analytics/manual-capture)
<NewsletterForm />