Free Items
Learn how to let players claim free items without a code through the Tokenz Free Items API.
The Free Items API helps you run giveaways where players receive a reward without entering a code and without paying. Your server checks a campaign's status, claims on behalf of a player, and then handles a redemption.completed webhook for fulfillment.
Overview
The claim flow includes:
- Status: Read a campaign's reward preview, remaining claims, and next reset time
- Claim: Record a successful claim for a specific player and create a redemption record
- Webhook fulfillment: Receive
redemption.completedand grant the reward in your game backend
Free items reuse the campaign model and the redemption.completed webhook that redemption codes use. The difference is that no code exists, your server supplies campaignId and playerId instead of a redemption code.
The full request and response schemas for both endpoints are in the Free Items API reference.
Authentication
Call these endpoints from your server using a secret API key. Enable the FreeItemStatus scope to read status and FreeItemClaim to claim. Keep the key on your backend and derive playerId from the authenticated player in your game.
Player identifiers
playerId is your own identifier for a player. Tokenz treats it as an opaque string and does not look it up in your game, so the value you send is the only thing that identifies the player.
Tokenz applies these rules, and nothing else:
- Leading and trailing whitespace is stripped
- The value cannot be blank
- The value can be at most 100 characters. A longer value is rejected with
request.decoding-failed, never silently shortened - The value is matched exactly, including letter case
Player IDs are case-sensitive. Player_123 and player_123 are distinct identifiers and hold separate claim allowances. Use the same stable identifier, including capitalization, on every request, from every code path that calls these endpoints. A player's own account ID from your game is usually the right choice.
Whitespace trimming is the only normalization Tokenz performs. Do not lowercase or otherwise change the case of an identifier before sending it, because that would merge two distinct players into one.
The same identifier is the same player everywhere. A player claiming from a different device, session or storefront is still the same player, and shares one allowance.
Campaign setup and management
Free item campaigns are created and managed in the Tokenz Dashboard. If you do not see free item campaigns in your dashboard yet, contact Tokenz support to confirm availability for your account.
Merchants use the Tokenz Dashboard to:
- Create campaigns with active periods and reward configuration
- Set a total claim limit and a per-player claim limit
- Choose a refresh cadence, so the per-player limit resets on a schedule
- Pause, resume, or end campaign operations
Campaign statuses
Free item campaigns use the same statuses as code campaigns:
draft: Work in progress. Claims are rejected. All fields are editable.scheduled: Published and queued. Becomes claimable at the configuredstartAttime.active: Campaign is live and claims can succeedpaused: Manually paused. Claims are rejected. Can be resumed toactiveended: Campaign is finished. Claims are rejected. This state is irreversible
An active campaign can accept claims. A scheduled campaign can also accept claims once startAt is reached, even before its stored status changes to active. Claims are rejected at or after endAt. Both are compared as absolute instants, so a campaign ending at midnight UTC stops accepting claims at that instant regardless of where the player is.
Claim limits
Two independent limits apply, and both are checked on every claim:
- Total claim limit: the maximum number of claims across all players for the whole campaign. Once reached, every further claim is rejected with
campaign.limit-reached. - Per-player claim limit: how many times one player can claim. Once reached, that player is rejected with
campaign.player-limit-reachedfor a one-time campaign, orcampaign.player-period-limit-reachedfor a recurring campaign. Other players are unaffected.
Per-player limits are enforced per playerId. The same player claiming from a different device or session is still the same player.
A rejected claim consumes nothing. If a player has already reached their own limit, the campaign's total claim count is not affected.
Recurring campaigns
A campaign can refresh the per-player limit on a schedule, so the same player can claim again in each new period. The cadence is chosen when the campaign is created:
| Cadence | Period starts |
|---|---|
| None (one-time) | Never resets. The per-player limit applies for the life of the campaign |
| Daily | Local midnight |
| Weekly | Monday at local midnight |
| Monthly | The first day of the month, at local midnight |
Periods are calculated in the campaign's own timezone, not the player's. Weekly periods always begin on Monday.
That timezone is not returned by the API, so do not try to work out a period boundary yourself. Use nextResetAt, which is already the correct absolute instant, and refresh status once it passes.
Only the per-player limit resets. The campaign's total claim limit is never restored by a period rollover.
nextResetAt in the claim and status responses is the exact instant the current period ends and the next one begins. Use it to render a countdown. It is absent for one-time campaigns. A claim made at or after nextResetAt belongs to the new period, provided the campaign is still available. A reset does not extend the campaign end time or replenish its total claim limit.
Claim lifecycle
- Your server calls
GET /v2/free-items/{campaignId}to show the reward and the player's remaining claims - Player chooses to claim in your UI
- Your server calls
POST /v2/free-items/claim - Tokenz returns a successful claim response
- Tokenz sends a
redemption.completedwebhook - Your backend fulfills the reward and should use
redemptionIdto guard against duplicate fulfillment
Get free item status
Status returns the campaign preview and reward preview, plus how many claims remain. The campaign must be active, or scheduled with its start time reached, and must not have expired. Exhausted limits return 0 rather than a limit error. A status check does not reserve a claim; availability can change before the claim request. Pass playerId to also receive that player's remaining claims for the current period.
curl --request GET \
--url 'https://api.tokenz.one/v2/free-items/campaign_1p4LPTRKB5Z_t?playerId=player_98765' \
--header 'Authorization: Bearer secret_test_YOUR_KEY_HERE'
Request fields
campaignId(required, path): The free item campaign to readplayerId(optional, query): Merchant player identifier. If provided, the response includes that player's remaining claims. Whitespace is stripped, the value cannot be blank, it is limited to 100 characters, and it is case-sensitive. See Player identifiers
Success response (200)
{
"campaign": {
"id": "campaign_1p4LPTRKB5Z_t",
"name": "Weekly Free Pack"
},
"rewardPreview": {
"name": "Marathon Energy Gift",
"imageUrl": "https://images.example.com/rewards/energy.png",
"quantity": 1
},
"campaignRemaining": 842,
"remainingForPlayer": 1,
"nextResetAt": "2026-09-14T00:00:00Z"
}
Response fields
campaignRemaining: Claims left for the whole campaign. Absent if the campaign has no total claim limitremainingForPlayer: Claims left for this player in the current period. Absent ifplayerIdwas not provided, or the campaign has no per-player limit. A player with no claims left receives0rather than an errornextResetAt: When the per-player limit next resets. Absent for one-time campaigns
Claim a free item
Claim creates a redemption record for a player. Your backend grants the reward when it processes the webhook. A successful claim requires the campaign to be claimable and both limits to have room.
curl --request POST \
--url https://api.tokenz.one/v2/free-items/claim \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer secret_test_YOUR_KEY_HERE' \
--data '{
"campaignId": "campaign_1p4LPTRKB5Z_t",
"playerId": "player_98765"
}'
Request fields
campaignId(required): The free item campaign to claim fromplayerId(required): Merchant player identifier. Whitespace is stripped, the value cannot be blank, it is limited to 100 characters, and it is case-sensitive. See Player identifiers
Success response (201)
{
"redemption": {
"redemptionId": "redemption_1p4LPTRKB5Z_t",
"playerId": "player_98765",
"campaign": {
"id": "campaign_1p4LPTRKB5Z_t",
"name": "Weekly Free Pack"
},
"reward": {
"skuRedemptionReward": {
"type": "EXTERNAL_SKU",
"sku": "ITEM_ENERGY_GIFT",
"name": "Marathon Energy Gift",
"quantity": 1
}
},
"redeemedAt": "2026-09-10T08:10:00Z",
"source": "FREE_ITEM_CLAIM"
},
"remainingForPlayer": 0,
"nextResetAt": "2026-09-14T00:00:00Z"
}
remainingForPlayer is the number of claims left after this claim, and is omitted when no per-player limit is configured. nextResetAt is omitted for one-time campaigns.
The redemption object has no code field, because no code was used. See Claim webhook.
Retries and duplicate fulfillment
The claim request has no idempotency-key field. Repeating a successful request can create another redemption if limits allow it.
If a claim response is lost, do not blindly retry: a second request can consume a second claim and draw the campaign's total down again.
A redemption.completed webhook for that campaign and player shows that a claim succeeded, and carries the redemptionId your lost response would have returned. Tying that webhook to one specific timed-out request is not always unambiguous, because the payload identifies the campaign and the player rather than your request. Correlate it against your own request log, and take extra care on campaigns where a player can hold more than one claim, such as recurring campaigns.
If neither the response nor a webhook is available, the public API currently provides no way to read a claim back, so the outcome cannot be confirmed from your side. Contact Tokenz support rather than retrying.
Deduplicate fulfillment by redemptionId. Deduplicating webhook deliveries does not prevent a second claim request.
Claim webhook
After a successful claim, Tokenz sends redemption.completed to webhook endpoints subscribed to this event. This is the same event that code redemptions use.
Verify the signature before you grant anything. Every delivery carries a Tokenz-Signature header, and a handler that skips verification will grant free items to anyone who finds or guesses its URL. Verify over the raw request body, then parse. See Secure your webhook endpoint for the steps. Treat the fields below as trustworthy only once the signature checks out.
{
"id": "f75ff6f0-bdf9-4b25-bb96-968f8733a7e3",
"object": "redemption.completed",
"createdAt": "2026-09-10T08:10:00Z",
"test": true,
"eventData": {
"type": "redemption",
"version": "v2",
"data": {
"redemption": {
"redemptionId": "redemption_1p4LPTRKB5Z_t",
"campaign": {
"id": "campaign_1p4LPTRKB5Z_t",
"name": "Weekly Free Pack"
},
"playerId": "player_98765",
"reward": {
"skuRedemptionReward": {
"type": "EXTERNAL_SKU",
"sku": "ITEM_ENERGY_GIFT",
"name": "Marathon Energy Gift",
"quantity": 1
}
},
"redeemedAt": "2026-09-10T08:10:00Z",
"source": "FREE_ITEM_CLAIM"
}
}
}
}
Telling free item claims apart from code redemptions
Free item claims and code redemptions both arrive on redemption.completed.
Fulfillment usually does not need to tell them apart. redemptionId, playerId and reward are present for both, and they are everything you need to grant the reward once to the right player. Prefer a single fulfillment path that reads those three fields.
When you do need to distinguish the two, for example for reporting:
sourceisFREE_ITEM_CLAIMfor a free item claim, andCODE_REDEMPTIONfor a redemption code. Those are the two values sent todaycodecarries the redeemed code for a code redemption, and is absent for a free item claim. Do not assume it is present
If source is missing or holds a value you do not recognise, do not guess. Fulfill from redemptionId, playerId and reward as usual, and flag the event for reconciliation.
If you already handle redemption.completed for redemption codes, review that handler before running your first free item campaign. A handler that reads code unconditionally will fail on the first claim.
Use the top-level event id for webhook delivery tracing, and use redemptionId to guard against duplicate fulfillment in your backend.
Delivery and retries
If your endpoint does not accept a delivery, Tokenz retries automatically, with increasing gaps and some randomization. Retries are scheduled within a window of up to 72 hours from the first attempt.
That window governs when retries are scheduled. It is not a promise that delivery succeeds inside it, and a scheduled attempt can still run later than planned. Do not depend on an exact number of attempts, or on exact retry times.
Two consequences for your handler:
- A delivery can arrive days after the claim. Keep your record of fulfilled
redemptionIdvalues in durable storage, not in a cache with a short expiry. A record that expires before the last retry will let the same reward be granted twice - The same
redemptionIdcan arrive more than once. Make fulfillment idempotent on it
Error handling
Free item endpoints return standard HTTP status codes with structured error objects.
Common errors
400 Bad Request(request.decoding-failed): Invalid campaign id, a blank or over-lengthplayerId, or a malformed request401 Unauthorized: Invalid or missing API key403 Forbidden: API key does not have the required scope404 Not Found(entity.not-found): Campaign not found, or it belongs to another merchant, or its mode does not match your API key422 Unprocessable Entity(campaign.kind-invalid): The campaign is not a free item campaign422 Unprocessable Entity(campaign.status-invalid): The campaign is draft, paused, ended, or scheduled before its start time422 Unprocessable Entity(campaign.expired): The campaign has reached its end time (an ended status can instead returncampaign.status-invalid)422 Unprocessable Entity(campaign.limit-reached): The campaign reached its total claim limit422 Unprocessable Entity(campaign.player-limit-reached): This player reached the per-player claim limit422 Unprocessable Entity(campaign.player-period-limit-reached): This player reached the claim limit for the current period. Retry afternextResetAt429 Too Many Requests: Rate limit exceeded. Both endpoints are rate limited per API key and, when supplied, per player500 Server Error: Unexpected server error
Testing
Use test mode to develop and test your free item integration:
- Use test API keys (prefixed with
secret_test_) to interact with test campaigns - Create test campaigns in the Tokenz Dashboard using test mode
- Test claims are fully isolated from live data. A test API key can only claim from test campaigns, and a live API key can only claim from live campaigns
- Test entity IDs include a
_tsuffix (e.g.,redemption_1p4LPTRKB5Z_t) - Test webhooks are delivered only to your test webhook endpoints, with
"test": truein the payload
Test a successful claim and webhook, an exhausted player limit, an exhausted total limit, a period reset, paused and expired campaigns, and duplicate webhook delivery. Confirm that the reward is granted once per redemptionId.