curl --request POST \
--url "https://api.seekeasy.ai/v1/event" \
--header "Authorization: Bearer YOUR_API_KEY" \
--header "Content-Type: application/json" \
--data '{
"event_data": {
"event_type": "post_video_play",
"place_id": "8a2830828b77fff:Rosewater Cafe",
"post_id": "post_xyz789",
"session_id": "sess_8f2b5a6d",
"slate_id": "b1f0c2d4-9e3a-4c17-8f2b-5a6d7e8f9012",
"surface": "expanded",
"position": 2,
"slate_size": 20,
"autoplay": false,
"muted": false
},
"occurred_at": "2026-06-01T12:00:00Z",
"idempotency_key": "evt-8f2b5a6d-0007"
}'
import requests
requests.post(
"https://api.seekeasy.ai/v1/event",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={
"event_data": {
"event_type": "post_video_play",
"place_id": "8a2830828b77fff:Rosewater Cafe",
"post_id": "post_xyz789",
"session_id": "sess_8f2b5a6d",
"slate_id": "b1f0c2d4-9e3a-4c17-8f2b-5a6d7e8f9012",
"surface": "expanded",
"position": 2,
"slate_size": 20,
"autoplay": False,
"muted": False,
},
"occurred_at": "2026-06-01T12:00:00Z",
"idempotency_key": "evt-8f2b5a6d-0007",
},
)
await fetch("https://api.seekeasy.ai/v1/event", {
method: "POST",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
event_data: {
event_type: "post_video_play",
place_id: "8a2830828b77fff:Rosewater Cafe",
post_id: "post_xyz789",
session_id: "sess_8f2b5a6d",
slate_id: "b1f0c2d4-9e3a-4c17-8f2b-5a6d7e8f9012",
surface: "expanded",
position: 2,
slate_size: 20,
autoplay: false,
muted: false,
},
occurred_at: "2026-06-01T12:00:00Z",
idempotency_key: "evt-8f2b5a6d-0007",
}),
});
{
"success": true
}
{
"detail": "event_data.post_impression.slate_id: Field required"
}
API Reference
Event Tracking
POST /v1/event - Report how users engage with the content we serve you
POST
/
v1
/
event
curl --request POST \
--url "https://api.seekeasy.ai/v1/event" \
--header "Authorization: Bearer YOUR_API_KEY" \
--header "Content-Type: application/json" \
--data '{
"event_data": {
"event_type": "post_video_play",
"place_id": "8a2830828b77fff:Rosewater Cafe",
"post_id": "post_xyz789",
"session_id": "sess_8f2b5a6d",
"slate_id": "b1f0c2d4-9e3a-4c17-8f2b-5a6d7e8f9012",
"surface": "expanded",
"position": 2,
"slate_size": 20,
"autoplay": false,
"muted": false
},
"occurred_at": "2026-06-01T12:00:00Z",
"idempotency_key": "evt-8f2b5a6d-0007"
}'
import requests
requests.post(
"https://api.seekeasy.ai/v1/event",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={
"event_data": {
"event_type": "post_video_play",
"place_id": "8a2830828b77fff:Rosewater Cafe",
"post_id": "post_xyz789",
"session_id": "sess_8f2b5a6d",
"slate_id": "b1f0c2d4-9e3a-4c17-8f2b-5a6d7e8f9012",
"surface": "expanded",
"position": 2,
"slate_size": 20,
"autoplay": False,
"muted": False,
},
"occurred_at": "2026-06-01T12:00:00Z",
"idempotency_key": "evt-8f2b5a6d-0007",
},
)
await fetch("https://api.seekeasy.ai/v1/event", {
method: "POST",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
event_data: {
event_type: "post_video_play",
place_id: "8a2830828b77fff:Rosewater Cafe",
post_id: "post_xyz789",
session_id: "sess_8f2b5a6d",
slate_id: "b1f0c2d4-9e3a-4c17-8f2b-5a6d7e8f9012",
surface: "expanded",
position: 2,
slate_size: 20,
autoplay: false,
muted: false,
},
occurred_at: "2026-06-01T12:00:00Z",
idempotency_key: "evt-8f2b5a6d-0007",
}),
});
{
"success": true
}
{
"detail": "event_data.post_impression.slate_id: Field required"
}
Tell us what users do with the posts returned by Place Lookup. These events are
how we learn which content earns attention and which drives conversions, which in turn is what lets us
return better posts for your venues over time.
Send one event per request. A valid event returns
Send one whenever a post reaches 50% of its tile on screen for one continuous second — an
Optional
Response —
Send one when a video’s playback begins, whether autoplayed or user-initiated. A separate
Optional
Response —
Send one each time playback stops, not only when a video finishes — that’s the video reaching its
end (
Optional
Response —
Optional
Response —
Send one when the user taps your call to action — “Book a Table”, or whatever the equivalent is on
your surface. The slate fields are optional here because a call to action doesn’t have to sit on a
post:
Optional
Response —
Sent when a booking is made or an order is placed.
Optional
Response —
Settles a conversion, and doubles as the correction channel: a
Optional
Response —
202 Accepted.
The video events must include a
slate_id, which comes from the
Place Lookup response.How a slate flows
1
Fetch the posts
Call
GET /v1/place once for each set of posts you’re about to show. The
response includes a slate_id.2
Keep the slate_id
Store it alongside the posts in your page state. It belongs to the page, not to any
individual post — every event from that set of posts carries the same
slate_id.3
Report interactions
As the user views, plays and moves through the posts, send events with the slate context
below. Echo
place_id, post_id and slate_id exactly as we served them.4
Report conversions
Send
conversion_cta_click when they tap your call to action, conversion when it goes
through, and conversion_outcome once you know how it turned out.Authentication
string
required
Bearer <your API key>.Request body
object
required
The event itself.
event_type selects which shape applies — the type-specific fields are listed
under Event types.Hide event_data
Hide event_data
string
required
One of
post_impression, post_video_play, post_video_end, post_advance,
conversion_cta_click, conversion or conversion_outcome.string
required
The
place_id from the /v1/place response, echoed exactly. Max 300 characters.string
The post involved, echoed exactly as served. Max 200 characters. Required on the four
interaction events (
post_impression, post_video_play, post_video_end, post_advance);
optional on conversion_cta_click and conversion; not used on conversion_outcome.string
Your identifier for the user’s browsing session. Max 200 characters. Required on every
event type that carries it — that’s all of them except
conversion_outcome, which reaches the
session through the conversion_id it settles.It should span page views and live as long as you can reasonably keep it.string
The
slate_id from the /v1/place response that delivered this post. Max 200 characters.
Required on the four interaction events; optional on conversion_cta_click and
conversion.string
Where the interaction happened —
inline or expanded. Required on the four interaction
events; optional on conversion_cta_click.inline is the post shown in place within the slate, typically small, muted and often
autoplayed. expanded is a full-screen or lightbox player the user deliberately opened. A
three-second muted tile play and a thirty-second deliberate watch are both “a play”, so we
report the two surfaces separately and never pool them.Layout isn’t a surface — a grid or a vertical list is still inline.integer
Zero-based index of this post within the slate.
integer
Number of posts in the slate at the moment of the event.
string
ISO-8601 timestamp of when the event happened on your side.
string
Deduplicates retries: two requests with the same key are recorded once. Without it, a retried
request is recorded as a second event. Max 200 characters.
string
Optional stable identifier for the user. Max 200 characters.
string
Your own environment label, e.g.
staging. Lets us separate your test traffic from real traffic
when both point at the production URL. Max 100 characters.Event types
Each event carries the shared envelope described under Request body; the sections below list whatevent_data itself must look like for each event_type. Field descriptions are
scoped to how they behave for that event.
post_impression — a post became visible
Send one whenever a post reaches 50% of its tile on screen for one continuous second — an
IntersectionObserver at threshold: 0.5 plus a one-second timer is the usual implementation.
Fire it for the inline tile, and again with surface: "expanded" when the post opens in the
expanded player.
Required event_data fields
| Field | Notes |
|---|---|
event_type | Must be "post_impression". |
place_id | Seekeasy place identifier, echoed exactly from the /v1/place response. |
post_id | Seekeasy post identifier of the post that became visible, echoed exactly. |
session_id | Your identifier for the user’s browsing session. |
slate_id | The slate_id from the /v1/place response that delivered this post. |
surface | Where the impression fired — "inline" or "expanded". |
event_data fields
| Field | Notes |
|---|---|
position | Zero-based index of this post within the slate. |
slate_size | Number of posts in the slate at the moment of the event. |
event_data
{
"event_type": "post_impression",
"place_id": "8a2830828b77fff:Rosewater Cafe",
"post_id": "post_xyz789",
"session_id": "sess_8f2b5a6d",
"slate_id": "b1f0c2d4-9e3a-4c17-8f2b-5a6d7e8f9012",
"surface": "inline",
"position": 2,
"slate_size": 20
}
202 Accepted:
{
"success": true
}
post_video_play — playback started
Send one when a video’s playback begins, whether autoplayed or user-initiated. A separate
post_video_end will report where the view stopped.
Required event_data fields
| Field | Notes |
|---|---|
event_type | Must be "post_video_play". |
place_id | Seekeasy place identifier, echoed from /v1/place. |
post_id | The post whose video started playing. |
session_id | Browsing session identifier. |
slate_id | The slate_id from the /v1/place response that delivered this post. |
surface | "inline" or "expanded". |
event_data fields
| Field | Notes |
|---|---|
position | Zero-based index of this post within the slate. |
slate_size | Number of posts in the slate at the moment of the event. |
autoplay | true when playback began without an explicit user action. Autoplayed views are much weaker interest signals than deliberate ones. |
muted | true when playback began with audio muted. |
event_data
{
"event_type": "post_video_play",
"place_id": "8a2830828b77fff:Rosewater Cafe",
"post_id": "post_xyz789",
"session_id": "sess_8f2b5a6d",
"slate_id": "b1f0c2d4-9e3a-4c17-8f2b-5a6d7e8f9012",
"surface": "expanded",
"position": 2,
"slate_size": 20,
"autoplay": false,
"muted": false
}
202 Accepted:
{
"success": true
}
post_video_end — playback stopped
Send one each time playback stops, not only when a video finishes — that’s the video reaching its
end (ended), the viewer moving to another post (advanced), dismissing the expanded player
(closed), or leaving the page (navigated_away). A pause isn’t a stop: hold the event while
playback is paused, so a viewer who resumes is reported once when the view really ends, and one who
never comes back is reported on unload as navigated_away.
Required event_data fields
| Field | Notes |
|---|---|
event_type | Must be "post_video_end". |
place_id | Seekeasy place identifier, echoed from /v1/place. |
post_id | The post whose playback stopped. |
session_id | Browsing session identifier. |
slate_id | The slate_id from the /v1/place response that delivered this post. |
surface | "inline" or "expanded". |
reason | Why playback stopped. One of "ended", "advanced", "closed", "navigated_away". Required — without it a completion and an abandonment are the same event. |
event_data fields
| Field | Notes |
|---|---|
position | Zero-based index of this post within the slate. |
slate_size | Number of posts in the slate at the moment of the event. |
watch_time_seconds | Cumulative seconds watched. Not capped at the video’s duration — a looping video accumulates past it, so don’t clamp it. |
percent_watched | Cumulative watch time as a percentage of the video’s duration. Like watch_time_seconds, not capped at 100 — a looping video accumulates past it, so 250 means two and a half passes. On an abandonment this is where the viewer stopped. |
loop_count | Times the video restarted after the first pass. 0 means it played through at most once. |
video_duration_seconds | Duration of the video as played, in seconds. Must be greater than zero. |
unmuted | true if the viewer turned audio on at any point during this view. muted on post_video_play only describes how playback started. |
navigated_away fires while the page is unloading, so send it with
navigator.sendBeacon()
or fetch(..., { keepalive: true }). An ordinary request gets cancelled on unload.Finished
{
"event_type": "post_video_end",
"place_id": "8a2830828b77fff:Rosewater Cafe",
"post_id": "post_xyz789",
"session_id": "sess_8f2b5a6d",
"slate_id": "b1f0c2d4-9e3a-4c17-8f2b-5a6d7e8f9012",
"surface": "expanded",
"position": 2,
"reason": "ended",
"watch_time_seconds": 42.5,
"percent_watched": 152,
"loop_count": 1,
"video_duration_seconds": 28.0,
"unmuted": true
}
Abandoned
{
"event_type": "post_video_end",
"place_id": "8a2830828b77fff:Rosewater Cafe",
"post_id": "post_xyz789",
"session_id": "sess_8f2b5a6d",
"slate_id": "b1f0c2d4-9e3a-4c17-8f2b-5a6d7e8f9012",
"surface": "inline",
"position": 2,
"reason": "advanced",
"watch_time_seconds": 3.2,
"percent_watched": 11,
"loop_count": 0,
"video_duration_seconds": 28.0,
"unmuted": false
}
202 Accepted:
{
"success": true
}
post_advance — the user moved to another post
post_id and position describe the post being advanced from; to_position gives the
destination. Applies on both surfaces — swiping inside the full-screen player counts too.
Required event_data fields
| Field | Notes |
|---|---|
event_type | Must be "post_advance". |
place_id | Seekeasy place identifier, echoed from /v1/place. |
post_id | The post the user was advancing from. |
session_id | Browsing session identifier. |
slate_id | The slate_id from the /v1/place response that delivered the slate. |
surface | "inline" or "expanded". |
method | How the advance happened. One of "next_click", "prev_click", "swipe", "scroll", "autoadvance", "tap". Use "scroll" for layouts with no discrete next control, such as a grid or vertical list. Required — an autoadvance is not a signal of interest, so an advance whose method is unknown is not usable. |
tap is accepted for controls that don’t distinguish forward from back, but it carries no
direction. Prefer next_click or prev_click whenever you know which control was used, and send
to_position so the destination is explicit either way.event_data fields
| Field | Notes |
|---|---|
position | Zero-based index within the slate of the post advanced from. |
slate_size | Number of posts in the slate at the moment of the event. |
from_position | Zero-based index advanced from. Same value as position when both are sent. |
to_position | Zero-based index advanced to. |
event_data
{
"event_type": "post_advance",
"place_id": "8a2830828b77fff:Rosewater Cafe",
"post_id": "post_xyz789",
"session_id": "sess_8f2b5a6d",
"slate_id": "b1f0c2d4-9e3a-4c17-8f2b-5a6d7e8f9012",
"surface": "inline",
"position": 2,
"method": "swipe",
"from_position": 2,
"to_position": 3
}
202 Accepted:
{
"success": true
}
conversion_cta_click — the user tapped your call to action
Send one when the user taps your call to action — “Book a Table”, or whatever the equivalent is on
your surface. The slate fields are optional here because a call to action doesn’t have to sit on a
post:
- Tapped on a post — a button on a tile, or one inside the expanded player. Send
post_id,slate_id,surfaceandpositionso the tap is attributed to the content that drove it. - Tapped on the page — a header, footer or sidebar button belonging to the place rather than
to any one post. Send
place_idandsession_idon their own.
event_data fields
| Field | Notes |
|---|---|
event_type | Must be "conversion_cta_click". |
place_id | Seekeasy place identifier, echoed from /v1/place. |
session_id | Browsing session identifier. |
event_data fields
| Field | Notes |
|---|---|
post_id | The post the user was viewing when they tapped, when the tap sat on a post. |
slate_id | The slate_id from the /v1/place response, when the tap sat on a post. |
surface | "inline" or "expanded", when the tap sat on a post. |
position | Zero-based slate index of the attributed post, when known. |
slate_size | Number of posts in the slate at the moment of the event. |
event_data
{
"event_type": "conversion_cta_click",
"place_id": "8a2830828b77fff:Rosewater Cafe",
"post_id": "post_xyz789",
"session_id": "sess_8f2b5a6d",
"slate_id": "b1f0c2d4-9e3a-4c17-8f2b-5a6d7e8f9012",
"surface": "expanded",
"position": 2
}
202 Accepted:
{
"success": true
}
conversion — the conversion happened
Sent when a booking is made or an order is placed. conversion_id is what a later
conversion_outcome matches on to settle this conversion, and session_id ties it back to the
browsing that led to it. post_id and slate_id stay optional — a conversion needn’t originate
from a particular post — but sending them is what attributes it to the content that drove it.
Required event_data fields
| Field | Notes |
|---|---|
event_type | Must be "conversion". |
place_id | Seekeasy place identifier, echoed from /v1/place. |
session_id | Browsing session identifier. Ties the conversion back to the browsing that produced it, even when it settles on a different page. |
conversion_id | Your identifier for the conversion. Must be stable across this event and any later conversion_outcome that settles it. |
event_data fields
| Field | Notes |
|---|---|
post_id | The post the user was viewing when they converted, when attributable. |
slate_id | The slate the conversion is attributed to, when known. |
position | Zero-based slate index of the attributed post, when known. |
quantity | Units on the conversion — covers on a reservation, items on an order (minimum 1). This is the figure at conversion time; what was actually fulfilled comes through on conversion_outcome. |
scheduled_for | ISO-8601 date and time the conversion is scheduled for, where that differs from when it was made — a reservation’s sitting, a delivery slot. |
event_data
{
"event_type": "conversion",
"place_id": "8a2830828b77fff:Rosewater Cafe",
"conversion_id": "ot_res_44813",
"post_id": "post_xyz789",
"session_id": "sess_8f2b5a6d",
"slate_id": "b1f0c2d4-9e3a-4c17-8f2b-5a6d7e8f9012",
"position": 2,
"quantity": 4,
"scheduled_for": "2026-06-14T19:30:00Z"
}
202 Accepted:
{
"success": true
}
conversion_outcome — the outcome of a conversion
Settles a conversion, and doubles as the correction channel: a no_show or cancelled reverses
what an earlier conversion recorded. This event carries no session_id, post_id, slate_id or
surface of its own — it reaches the browsing context through the conversion_id it settles.
Required event_data fields
| Field | Notes |
|---|---|
event_type | Must be "conversion_outcome". |
place_id | Seekeasy place identifier, echoed from /v1/place. |
conversion_id | Matches the conversion_id sent on the corresponding conversion event. |
status | Final outcome. One of "fulfilled", "no_show", "cancelled". |
event_data fields
| Field | Notes |
|---|---|
quantity_fulfilled | Units actually fulfilled — diners seated, items delivered. Zero or omitted when nothing was. |
revenue_amount_minor | Revenue in the minor unit of revenue_currency (e.g. cents), as a non-negative integer — never a float. status carries the direction, so a reversal is a cancelled/no_show event rather than a negative amount. |
revenue_currency | ISO 4217 code, e.g. "USD". Required whenever revenue_amount_minor is set. |
event_data
{
"event_type": "conversion_outcome",
"place_id": "8a2830828b77fff:Rosewater Cafe",
"conversion_id": "ot_res_44813",
"status": "fulfilled",
"quantity_fulfilled": 4,
"revenue_amount_minor": 24500,
"revenue_currency": "USD"
}
202 Accepted:
{
"success": true
}
Fulfilment and revenue figures currently feed analytics on a best-effort path. Treat them as
directional rather than a system of record until we tell you otherwise.
Response
boolean
Always
true on a 202. The event has been validated and queued.Errors
| Status | Meaning |
|---|---|
401 | No API key was sent. Add Authorization: Bearer <your API key>. |
403 | The key is invalid, or is missing permission to record events. |
422 | Validation failed. The body names the offending field, e.g. event_data.post_impression.slate_id: Field required. |
Retry
5xx responses and network failures with the same idempotency_key. A 422 isn’t
worth retrying — it will fail identically until the payload changes.curl --request POST \
--url "https://api.seekeasy.ai/v1/event" \
--header "Authorization: Bearer YOUR_API_KEY" \
--header "Content-Type: application/json" \
--data '{
"event_data": {
"event_type": "post_video_play",
"place_id": "8a2830828b77fff:Rosewater Cafe",
"post_id": "post_xyz789",
"session_id": "sess_8f2b5a6d",
"slate_id": "b1f0c2d4-9e3a-4c17-8f2b-5a6d7e8f9012",
"surface": "expanded",
"position": 2,
"slate_size": 20,
"autoplay": false,
"muted": false
},
"occurred_at": "2026-06-01T12:00:00Z",
"idempotency_key": "evt-8f2b5a6d-0007"
}'
import requests
requests.post(
"https://api.seekeasy.ai/v1/event",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={
"event_data": {
"event_type": "post_video_play",
"place_id": "8a2830828b77fff:Rosewater Cafe",
"post_id": "post_xyz789",
"session_id": "sess_8f2b5a6d",
"slate_id": "b1f0c2d4-9e3a-4c17-8f2b-5a6d7e8f9012",
"surface": "expanded",
"position": 2,
"slate_size": 20,
"autoplay": False,
"muted": False,
},
"occurred_at": "2026-06-01T12:00:00Z",
"idempotency_key": "evt-8f2b5a6d-0007",
},
)
await fetch("https://api.seekeasy.ai/v1/event", {
method: "POST",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
event_data: {
event_type: "post_video_play",
place_id: "8a2830828b77fff:Rosewater Cafe",
post_id: "post_xyz789",
session_id: "sess_8f2b5a6d",
slate_id: "b1f0c2d4-9e3a-4c17-8f2b-5a6d7e8f9012",
surface: "expanded",
position: 2,
slate_size: 20,
autoplay: false,
muted: false,
},
occurred_at: "2026-06-01T12:00:00Z",
idempotency_key: "evt-8f2b5a6d-0007",
}),
});
{
"success": true
}
{
"detail": "event_data.post_impression.slate_id: Field required"
}