Recipes
Common Prism queries. Each is POSTed to /v1/offers with your Authorization: Bearer <token> and Content-Type: application/json — see the Overview for the full request shape. Pass the player ID as a variable so the query is reusable.
Fetch offers for a player
query GetOffers($playerId: String!) {
offers(player_id: $playerId) {
id
name
total_payout_usd
}
}
{ "playerId": "user-123" }
Include creative details
Select nested fields to render the offer in your UI:
query GetOffersWithCreatives($playerId: String!) {
offers(player_id: $playerId) {
id
name
total_payout_usd
creatives {
name
description
}
}
}
Fetch a player's engaged offers
playerOffers returns the offers a player has already engaged with, both in progress and
completed, most recently engaged first. Each goal carries its own completion state, so you can
render partial progress on a multi-goal offer:
query GetPlayerOffers($playerId: String!, $after: String) {
playerOffers(player_id: $playerId, limit: 50, after: $after) {
offers {
id
name
status
started_at
availability
goals {
name
is_completed
completed_at
}
}
next_cursor
has_more
}
}
{ "playerId": "user-123" }
Two fields describe availability and progress, and they answer different questions. status is
the player's progress, IN_PROGRESS or COMPLETED. availability is the state of the offer
itself: an offer that has since been disabled or deleted still appears here, because a player who
already started it can still finish it.
Paginate on has_more, not on the number of offers returned
A page can come back with fewer than limit offers and still not be the last page, so the length
of offers is not an end-of-list signal. Loop until has_more is false, passing the previous
response's next_cursor as after:
const pages = [];
let after = null;
do {
const response = await fetch("https://prism.adgem.com/v1/offers", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
query: GET_PLAYER_OFFERS,
variables: { playerId: "user-123", after },
}),
});
const { data, errors } = await response.json();
if (errors) throw new Error(errors[0].message);
pages.push(...data.playerOffers.offers);
after = data.playerOffers.next_cursor;
} while (after !== null);
Omit after on the first request. next_cursor is null on the final page, which is the same
signal as has_more being false.
This list covers the last 120 days
playerOffers reports engagements from the last 120 days, measured from when the player engaged
with the offer. Older engagements drop off the list, including completed ones, so treat it as a
recent-activity view rather than a lifetime history. The same bound applies to the offer history
shown in AdGem's own offerwall, so a player sees the same window in both places.
Show attribution status
After a player clicks an offer, the advertiser has to verify the install before any goal can pay
out. Until that happens, a player who never installed, or who installed but isn't eligible, has no
way of knowing, and may keep working through the offer only to find out later through support.
AdGem's own offerwall handles this by showing the player an attribution status, and you can build
the same flow with playerOffers.
Request each offer's started_at and links, and, on its goals, is_attribution and
is_completed:
query GetAttributionStatus($playerId: String!) {
playerOffers(player_id: $playerId) {
offers {
id
name
status
started_at
links {
click_url
support_url
}
goals {
name
is_attribution
is_completed
}
}
next_cursor
has_more
}
}
{ "playerId": "user-123" }
An offer the player clicked but hasn't been attributed for yet comes back as IN_PROGRESS, with
its attribution goal still showing is_completed: false:
{
"data": {
"playerOffers": {
"offers": [
{
"id": "123456789012345678",
"name": "Play Game X to level 10",
"status": "IN_PROGRESS",
"started_at": "2026-08-14T09:12:44Z",
"links": {
"click_url": "https://api.adgem.com/v1/click?all=1&appid=12345&cid=12345&playerid=user-123",
"support_url": "https://api.adgem.com/support/12345/12345/user-123"
},
"goals": [
{ "name": "Install and open", "is_attribution": true, "is_completed": false },
{ "name": "Reach level 10", "is_attribution": false, "is_completed": false }
]
}
],
"next_cursor": null,
"has_more": false
}
}
}
Once the attribution goal shows is_completed: true, the install is verified and you can stop
showing a status. Until then, use the time since started_at to pick what to tell the player. If
an offer has no goal marked is_attribution, there is nothing to verify against, so show no
status at all rather than treating the offer as verified:
const RETRY_AFTER_MS = 10 * 60 * 1000; // 10 minutes
function attributionStatus(offer, now = Date.now()) {
const attributionGoal = offer.goals.find((goal) => goal.is_attribution);
if (!attributionGoal) return null; // no attribution goal: show no status
if (attributionGoal.is_completed) return "VERIFIED";
const elapsed = now - Date.parse(offer.started_at);
return elapsed < RETRY_AFTER_MS ? "PENDING" : "NOT_VERIFIED";
}
playerOffers reflects attribution when you query it, so query it again to pick up a change, for
example when the player returns to the offer or reopens your offerwall.
NOT_VERIFIED isn't final. Some conversions post well after 10 minutes, because of the
advertiser's tracking setup (typically a 0–3 hour delay) or AdGem's anti-fraud review (see
Postback timing). An offer can
move from NOT_VERIFIED to VERIFIED once its attribution goal completes, so keep re-querying
rather than treating NOT_VERIFIED as the player's final status.
What to tell the player
To give players the same experience across publishers, use the copy from AdGem's own offerwall:
| Status | When | Message | Actions |
|---|---|---|---|
PENDING | Less than 10 minutes since started_at, attribution goal not completed | "To be eligible for rewards, completion must be verified." | A More info link that opens: "If you have completed this, there may be a delay to verify this. Please check back in 10 minutes." |
NOT_VERIFIED | 10 minutes or more since started_at, attribution goal still not completed | "We could not verify your eligibility. This could be due to: You have downloaded this before / The offer provider has determined you are not a new user / You have not downloaded and opened the app" | A Try Again button that sends the player back through links.click_url, plus a More info link to the offer's support page at links.support_url. |
These are display states only. Don't hide, disable, or block the offer based on them. A late install can still pay out, as long as the advertiser's tracker still credits the click inside its attribution window, so the player should always be able to continue.
AdGem's offerwall also shows "Not eligible for rewards for this offer." when a click has gone a
set number of days without attribution. Like the states above, it's display only: nothing changes
on AdGem's side, and playerOffers still returns the offer as IN_PROGRESS. There's no separate
"not eligible" status to read today, so stop at the NOT_VERIFIED state above. An offer only
leaves the list when its transaction expires, 120 days after the click.
Tracking links
Prism also exposes a links query for a player's tracking links — select the fields you need on it the same way as offers.
GraphQL returns exactly the fields you request, so ask for only what you need. GraphQL is also introspective, so you can explore the available queries, fields, and types directly from any GraphQL client.