Engagement & analytics
Read aggregate engagement counts and recent activity for a render, and record partner-side events.
Read aggregate engagement counts and recent activity for a render, and record partner-side events from your own viewer UI. Engagement tracks who viewed, downloaded, printed, or was emailed a rendered document.
GET /v1/renders/:id/engagement
POST /v1/renders/:id/events:id is the render id (UUIDv7) within the authenticated project.
Quick Start
curl
# Read the engagement summary
curl https://api.craftkit.dev/v1/renders/0193c2c3/engagement \
-H "Authorization: Bearer $CRAFTKIT_API_KEY"
# Record a partner-side event
curl -X POST https://api.craftkit.dev/v1/renders/0193c2c3/events \
-H "Authorization: Bearer $CRAFTKIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "eventType": "downloaded", "metadata": { "source": "dashboard" } }'Node.js
const headers = { Authorization: `Bearer ${process.env.CRAFTKIT_API_KEY}` };
const summary = await (
await fetch('https://api.craftkit.dev/v1/renders/0193c2c3/engagement', { headers })
).json();
console.log(summary.counts.viewed, summary.linkOpens, summary.total);
const res = await fetch('https://api.craftkit.dev/v1/renders/0193c2c3/events', {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ eventType: 'printed', metadata: { source: 'dashboard' } }),
});
const { recorded } = await res.json();Python
import os, requests
headers = {"Authorization": f"Bearer {os.environ['CRAFTKIT_API_KEY']}"}
summary = requests.get(
"https://api.craftkit.dev/v1/renders/0193c2c3/engagement",
headers=headers,
).json()
res = requests.post(
"https://api.craftkit.dev/v1/renders/0193c2c3/events",
headers=headers,
json={"eventType": "viewed", "metadata": {"source": "dashboard"}},
)
recorded = res.json()["recorded"]Path parameters
| Field | Type | Description | Default |
|---|---|---|---|
id |
string | Render id (UUIDv7). Must belong to the API key's project. | — |
GET /v1/renders/:id/engagement
Returns aggregate counts across all event types, a linkOpens tally, the grand total, and the most recent events (newest first, capped at 25).
Response
{
"counts": {
"viewed": 12,
"downloaded": 3,
"printed": 1,
"email_opened": 4,
"email_sent": 2,
"share_created": 1,
"share_revoked": 0
},
"linkOpens": 8,
"total": 23,
"recent": [
{
"id": "0193c2d0-...",
"eventType": "viewed",
"actorKind": "recipient",
"shareId": "0193c2cf-...",
"sourceIp": "203.0.113.7",
"userAgent": "Mozilla/5.0 ...",
"metadata": null,
"createdAt": "2026-05-03T10:20:00.000Z"
}
]
}| Field | Type | Description |
|---|---|---|
counts |
object | Per-type event counts. Always includes all seven keys, zero-filled. |
counts.viewed |
integer | Document opens. |
counts.downloaded |
integer | PDF downloads. |
counts.printed |
integer | Print actions. |
counts.email_opened |
integer | Tracking-pixel opens on a sent email. |
counts.email_sent |
integer | Emails dispatched (partner audit). |
counts.share_created |
integer | Share links minted (partner audit). |
counts.share_revoked |
integer | Share links revoked (partner audit). |
linkOpens |
integer | Subset of viewed events that originated from a shared link (shareId is non-null). Distinguishes link traffic from in-dashboard views. |
total |
integer | Sum of all counts. |
recent |
array | Up to 25 most recent events, newest first. |
recent[].id |
string | Event id. |
recent[].eventType |
string | One of viewed, downloaded, printed, email_opened, email_sent, share_created, share_revoked. |
recent[].actorKind |
string | Who triggered it: recipient (the share recipient), partner (you, via the events endpoint or dashboard), or system (server-side automation). |
recent[].shareId |
string | null | The originating share link, when the event came through one. |
recent[].sourceIp |
string | null | Best-effort client IP (x-forwarded-for first hop, else x-real-ip). |
recent[].userAgent |
string | null | Client user-agent string. |
recent[].metadata |
object | null | Arbitrary key/value bag attached when the event was recorded. |
recent[].createdAt |
string | ISO-8601 timestamp. |
POST /v1/renders/:id/events
Records a partner-side engagement event from your own viewer UI. Every event written here is stamped actorKind: "partner" — you cannot record recipient-side events through this route. Recipient-side viewed/downloaded/printed events are written server-side by the public share page.
Request body
{
"eventType": "downloaded",
"metadata": { "source": "dashboard", "userId": "u_123" }
}| Field | Type | Description | Default |
|---|---|---|---|
eventType |
string | Required. One of viewed, downloaded, printed. Other engagement types (email_*, share_*) are recorded by the system, not this route. |
— |
metadata |
object | Optional free-form key/value bag (string keys, any JSON values) stored verbatim with the event. | — |
Dedupe.
viewed,downloaded, andprintedare deduped within a 5-minute window keyed on(shareId, eventType, sourceIp). A duplicate inside that window returns{ "recorded": false }and is not written.
Response
{ "recorded": true }| Field | Type | Description |
|---|---|---|
recorded |
boolean | true if the event was inserted, false if it was deduped within the 5-minute window. |
Errors
| HTTP | Code | Meaning | Fix |
|---|---|---|---|
| 400 | invalid_json |
POST body wasn't valid JSON | Check Content-Type and JSON.stringify |
| 400 | invalid_request |
POST body didn't match { eventType, metadata? } |
Inspect issues.fieldErrors; eventType must be viewed/downloaded/printed |
| 401 | unauthorized |
Missing, invalid, or revoked API key | Send a valid Authorization: Bearer key |
| 403 | forbidden |
Key's project no longer exists | Use a key from an active project |
| 404 | not_found |
No render with that id in this key's project | Check the id and the API key's project scope |
See Errors for the envelope shape and full code list.
Related
- GET /v1/renders/:id — poll the render and read its
downloadUrl - Download a render — authenticated PDF stream
- Errors — error envelope and retry semantics
- Authentication — bearer token format