curl --request GET \
--url https://api.privy.com/v1/contacts/{id}/events \
--header 'Authorization: Bearer <token>'import requests
url = "https://api.privy.com/v1/contacts/{id}/events"
headers = {"Authorization": "Bearer <token>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
fetch('https://api.privy.com/v1/contacts/{id}/events', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.privy.com/v1/contacts/{id}/events",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.privy.com/v1/contacts/{id}/events"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://api.privy.com/v1/contacts/{id}/events")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.privy.com/v1/contacts/{id}/events")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["Authorization"] = 'Bearer <token>'
response = http.request(request)
puts response.read_body{
"data": [
{
"type": "email-opened",
"occurred_at": "2026-08-20T14:22:00Z",
"campaign": {
"id": "Nw8K2rT5xY",
"title": "Welcome Series — Email 1",
"type": "flow"
},
"properties": {
"subject": "Take 10% Off"
}
},
{
"type": "signup",
"occurred_at": "2026-08-20T14:20:31Z",
"campaign": {
"id": "gk2P8Zw1Lq",
"title": "Welcome Popup",
"type": "display"
},
"properties": {
"redemption_code": "482910375563"
}
}
],
"pagination": {
"page": 1,
"per_page": 40,
"total_count": 2,
"total_pages": 1
}
}{
"error": {
"code": "unauthorized",
"message": "Bearer token is missing or invalid"
}
}{
"error": {
"code": "insufficient_scope",
"message": "Token does not have the required scope"
}
}{
"error": {
"code": "not_found",
"message": "Not found"
}
}{
"error": {
"code": "validation_failed",
"message": "One or more fields are invalid",
"details": [
{
"field": "email",
"message": "is required"
}
]
}
}{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded"
}
}List a contact's activity
Retrieve the contact’s activity timeline — signups, email and SMS sends, opens and clicks, orders, checkouts, tag changes — newest first. This is the same record the Privy dashboard’s contact page shows, so “why did this contact get X?” can be answered from the API alone: find the send, the signup that triggered it, and the display or campaign each traces to.
Each entry names its type (the event vocabulary below, as stored), when it happened, the
display or campaign it belongs to (when the event carries one, even one since deleted), and the
event’s raw properties, whose shape varies by type (the recipient IP and user agent recorded
on email opens and clicks, and the IP-derived location on signups, are not included).
By default the timeline covers the last 24 months; narrow it with occurred_after /
occurred_before (both bounds exclusive; a bare date means midnight at the start of that
day) or to one event type with type.
Required scope: contacts_read
curl --request GET \
--url https://api.privy.com/v1/contacts/{id}/events \
--header 'Authorization: Bearer <token>'import requests
url = "https://api.privy.com/v1/contacts/{id}/events"
headers = {"Authorization": "Bearer <token>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
fetch('https://api.privy.com/v1/contacts/{id}/events', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.privy.com/v1/contacts/{id}/events",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.privy.com/v1/contacts/{id}/events"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://api.privy.com/v1/contacts/{id}/events")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.privy.com/v1/contacts/{id}/events")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["Authorization"] = 'Bearer <token>'
response = http.request(request)
puts response.read_body{
"data": [
{
"type": "email-opened",
"occurred_at": "2026-08-20T14:22:00Z",
"campaign": {
"id": "Nw8K2rT5xY",
"title": "Welcome Series — Email 1",
"type": "flow"
},
"properties": {
"subject": "Take 10% Off"
}
},
{
"type": "signup",
"occurred_at": "2026-08-20T14:20:31Z",
"campaign": {
"id": "gk2P8Zw1Lq",
"title": "Welcome Popup",
"type": "display"
},
"properties": {
"redemption_code": "482910375563"
}
}
],
"pagination": {
"page": 1,
"per_page": 40,
"total_count": 2,
"total_pages": 1
}
}{
"error": {
"code": "unauthorized",
"message": "Bearer token is missing or invalid"
}
}{
"error": {
"code": "insufficient_scope",
"message": "Token does not have the required scope"
}
}{
"error": {
"code": "not_found",
"message": "Not found"
}
}{
"error": {
"code": "validation_failed",
"message": "One or more fields are invalid",
"details": [
{
"field": "email",
"message": "is required"
}
]
}
}{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded"
}
}Authorizations
Send either an API token or an OAuth access token as
Authorization: Bearer <token>. See the Authentication page for details.
Headers
Target a subaccount instead of the main connected account.
Pass a subaccount id from get_account's subaccounts list. Only direct subaccounts
of the connected account are reachable — omit this for the main account itself.
Path Parameters
The contact's unique identifier (returned as id in contact responses). Ids are opaque cus_-prefixed strings — a numeric id from a Privy dashboard URL is not a valid API id and will 404; list contacts, or look the contact up by ?email= or ?phone_number=, instead.
Query Parameters
Only return events of this type. Any type value a row carries is accepted (including names a merchant's own integration sent through the events ingest), so a type read off the timeline always filters. Common values: customer-subscribed, email-sent, email-delivered, email-opened, email-clicked, text-message-sent, text-message-delivered, view, new-session, redemption, signup, sms-signup, order-created, checkout-started, customer-tag-added, customer-tag-removed.
Only events after this instant (exclusive). ISO 8601 datetime or date. Defaults to 24 months before occurred_before — 24 months ago when neither bound is given.
Only events before this instant (exclusive). ISO 8601 datetime or date. Defaults to now.
Page number (starts at 1).
x >= 1Number of results per page. Values outside 10–100 are clamped to the nearest bound.
10 <= x <= 100Was this page helpful?