Create a campaign
const url = 'https://api.keliplay.com/v1/game-instances';const options = { method: 'POST', headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'}, body: '{"gameSlug":"spin-wheel","name":"Autumn wheel","channelId":"0192f3a1-0000-7000-8000-00000000c001","prize":{"label":"Free coffee","description":"One regular coffee at any store."},"winnerSelectionTrigger":"manual","maxPlaysPerParticipantPerDay":1,"playLimitTimezone":"Europe/London"}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request POST \ --url https://api.keliplay.com/v1/game-instances \ --header 'Authorization: Bearer <token>' \ --header 'Content-Type: application/json' \ --data '{ "gameSlug": "spin-wheel", "name": "Autumn wheel", "channelId": "0192f3a1-0000-7000-8000-00000000c001", "prize": { "label": "Free coffee", "description": "One regular coffee at any store." }, "winnerSelectionTrigger": "manual", "maxPlaysPerParticipantPerDay": 1, "playLimitTimezone": "Europe/London" }'Creates a campaign as a draft on a channel. A draft is never shown to players. Configure it, then activate it.
config may be left empty and filled in later. When you do send it, it
is checked against the game’s settings straight away. Each game’s
settings are described in the Games section.
scheduleAt is required when winnerSelectionTrigger is
auto_on_schedule.
Permission: campaigns.write. Setting liveFrom or liveUntil also
needs channels.manage. Plan feature: the game itself must be in your
plan (402 otherwise).
Authorizations
Section titled “Authorizations”Request Bodyrequired
Section titled “Request Bodyrequired”object
The game, from GET /v1/games.
The channel this campaign runs on.
The game’s own settings, such as wheel segments or quiz questions. The shape is different for each game. See the Games section for each game’s fields.
Each file a config names must be the right kind: a file under sounds a
sound, any other an image. A file from your media library is checked by its
type, any other address by its file extension. A mismatch is a 400 naming
the field, such as config.art.background.
object
What the campaign is giving away, as shown to winners.
object
Your own reference for the prize, such as a SKU.
When winners are drawn.
manual: when you call the draw operation.auto_on_session_end: after each play ends.auto_on_schedule: once, atscheduleAt.
How winners are picked from eligible entries. Defaults to the game’s
defaultWinnerAlgorithm.
random_uniform: every entry has the same chance.random_weighted: chance follows each entry’sweight.score_highest: highest scores win.
object
How equal scores are ordered, for score_highest.
Only entries made at or after this time count.
Only entries made before this time count.
When the draw runs, for auto_on_schedule.
When players can start playing. Null means from activation.
When play stops. Null means no end.
Most plays allowed per player. Null removes the limit. Leave out to keep the current value.
Most plays allowed per player. Null removes the limit. Leave out to keep the current value.
IANA time zone that decides when a day starts for the daily limit, such as Europe/London.
Example
{ "gameSlug": "spin-wheel", "name": "Autumn wheel", "channelId": "0192f3a1-0000-7000-8000-00000000c001", "prize": { "label": "Free coffee", "description": "One regular coffee at any store." }, "winnerSelectionTrigger": "manual", "maxPlaysPerParticipantPerDay": 1, "playLimitTimezone": "Europe/London"}Responses
Section titled “Responses”The new draft campaign.
object
object
draft: being set up. Players cannot see it.active: live on its channel, during its live window.paused: hidden for now. Plays and results are kept.archived: finished and put away. Can be restored.
The game’s own settings, such as wheel segments or quiz questions. The shape is different for each game. See the Games section for each game’s fields.
Each file a config names must be the right kind: a file under sounds a
sound, any other an image. A file from your media library is checked by its
type, any other address by its file extension. A mismatch is a 400 naming
the field, such as config.art.background.
object
Colours, images, font, wording, background, position and decorations for your games.
Each file must be the kind its field takes: logoUrl and backgroundUrl an image,
an animation or decoration the kind it names. A file from your media library is
checked by its type, any other address by its file extension. A mismatch is a
400 naming the field.
object
Six-digit hex colours, such as #3b8ad9.
object
An https URL, or a file from your media library.
The background image, or an animation’s still. An https URL, or a file from your media library.
A CSS font family list. Letters, digits, spaces, quotes, commas and hyphens.
Where to load the font from.
Your own wording for player-facing text, keyed by label. Each game lists its label keys in labelKeys.
object
Where the game sits when it fills a phone’s screen. Has no effect inside a website’s frame.
object
Defaults to center.
game keeps each game’s own width, wide allows at least 560 pixels, full uses the whole screen.
How the background is painted, on the pre-game page, the game and the reward page.
object
How backgroundUrl fills the screen. Defaults to cover.
The part of the image kept in view when it is cropped. Defaults to center.
Painted in place of the flat colors.bg.
object
A tint over the background, for legibility.
object
Percent.
Moving art from your media library. backgroundUrl is shown first, and
instead when motion is off, data is being saved, or it cannot play.
Videos play muted, only when the game fills a phone’s screen.
object
Artwork along the top and bottom edges. It never covers the game.
object
object
An https URL, or a file from your media library. An image repeats along its edge.
The height of its band, 48, 96 or 160 pixels.
object
An https URL, or a file from your media library. An image repeats along its edge.
The height of its band, 48, 96 or 160 pixels.
Whether a player can receive at most one coupon from this campaign. Set it with the reward.
How long a granted coupon stays valid. Set it with the reward.
When winners are drawn.
manual: when you call the draw operation.auto_on_session_end: after each play ends.auto_on_schedule: once, atscheduleAt.
How winners are picked from eligible entries. Defaults to the game’s
defaultWinnerAlgorithm.
random_uniform: every entry has the same chance.random_weighted: chance follows each entry’sweight.score_highest: highest scores win.
object
How equal scores are ordered, for score_highest.
Only entries made at or after this time count.
Only entries made before this time count.
When an auto_on_schedule draw runs.
The option that happened, on a prediction campaign.
When the campaign was first activated.
Examples
A campaign response
{ "gameInstance": { "id": "0192f3a1-0000-7000-8000-0000000ca001", "gameId": "0192f3a1-0000-7000-8000-0000000a9001", "gameSlug": "spin-wheel", "name": "Autumn wheel", "status": "active", "channelId": "0192f3a1-0000-7000-8000-00000000c001", "config": { "segments": [ { "id": "segment-1", "label": "Free coffee", "weight": 1 }, { "id": "segment-2", "label": "Try again", "weight": 4 } ] }, "prize": { "label": "Free coffee", "description": "One regular coffee at any store." }, "theme": null, "grantOncePerParticipant": true, "rewardValidForSeconds": 2592000, "winnerSelectionTrigger": "manual", "winnerSelectionAlgorithm": "random_weighted", "winnerSelectionConfig": { "maxWinners": 3 }, "scheduleAt": null, "resolvedOptionId": null, "resolvedAt": null, "maxPlaysPerParticipant": null, "maxPlaysPerParticipantPerDay": 1, "playLimitTimezone": "Europe/London", "liveFrom": "2026-10-01T00:00:00.000Z", "liveUntil": "2026-10-31T23:59:59.000Z", "activatedAt": "2026-09-30T12:00:00.000Z", "createdAt": "2026-09-25T09:00:00.000Z", "updatedAt": "2026-09-30T12:00:00.000Z" }}The request is not valid. details lists each field that failed, where there is one.
object
object
Stable and safe to branch on.
invalid_request(400): the request is not valid.unauthorized(401): missing, malformed or revoked API key.upgrade_required(402): not in your plan, or a plan limit is reached.forbidden(403): the key lacks a permission, or the action is not allowed.not_found(404): nothing with that id in your account.conflict(409): clashes with the current state.confirmation_required(409): repeat withconfirm=true. Folder deletion only.rate_limited(429): wait forRetry-Afterseconds.internal_error(500) andservice_unavailable(503): try again later.
Written for a person. May change; do not parse it.
object
The failing field, dot separated, such as prize.label.
object
Example
{ "error": { "code": "invalid_request", "message": "Invalid request json.", "details": [ { "path": "name", "message": "Too small, expected string to have >=1 characters" } ] }}The API key is missing, malformed or revoked.
object
object
Stable and safe to branch on.
invalid_request(400): the request is not valid.unauthorized(401): missing, malformed or revoked API key.upgrade_required(402): not in your plan, or a plan limit is reached.forbidden(403): the key lacks a permission, or the action is not allowed.not_found(404): nothing with that id in your account.conflict(409): clashes with the current state.confirmation_required(409): repeat withconfirm=true. Folder deletion only.rate_limited(429): wait forRetry-Afterseconds.internal_error(500) andservice_unavailable(503): try again later.
Written for a person. May change; do not parse it.
object
The failing field, dot separated, such as prize.label.
object
Example
{ "error": { "code": "unauthorized", "message": "Invalid or revoked API key." }}Your plan does not include this feature, or you have reached a plan limit.
object
object
Stable and safe to branch on.
invalid_request(400): the request is not valid.unauthorized(401): missing, malformed or revoked API key.upgrade_required(402): not in your plan, or a plan limit is reached.forbidden(403): the key lacks a permission, or the action is not allowed.not_found(404): nothing with that id in your account.conflict(409): clashes with the current state.confirmation_required(409): repeat withconfirm=true. Folder deletion only.rate_limited(429): wait forRetry-Afterseconds.internal_error(500) andservice_unavailable(503): try again later.
Written for a person. May change; do not parse it.
object
The failing field, dot separated, such as prize.label.
object
Example
{ "error": { "code": "upgrade_required", "message": "Rewards is not included in your plan. Ask whoever manages your account to add it." }}The API key does not carry the permission this needs, or the request is not allowed.
object
object
Stable and safe to branch on.
invalid_request(400): the request is not valid.unauthorized(401): missing, malformed or revoked API key.upgrade_required(402): not in your plan, or a plan limit is reached.forbidden(403): the key lacks a permission, or the action is not allowed.not_found(404): nothing with that id in your account.conflict(409): clashes with the current state.confirmation_required(409): repeat withconfirm=true. Folder deletion only.rate_limited(429): wait forRetry-Afterseconds.internal_error(500) andservice_unavailable(503): try again later.
Written for a person. May change; do not parse it.
object
The failing field, dot separated, such as prize.label.
object
Example
{ "error": { "code": "forbidden", "message": "Your role does not allow this (draws.run)." }}Nothing with that id exists in your account.
object
object
Stable and safe to branch on.
invalid_request(400): the request is not valid.unauthorized(401): missing, malformed or revoked API key.upgrade_required(402): not in your plan, or a plan limit is reached.forbidden(403): the key lacks a permission, or the action is not allowed.not_found(404): nothing with that id in your account.conflict(409): clashes with the current state.confirmation_required(409): repeat withconfirm=true. Folder deletion only.rate_limited(429): wait forRetry-Afterseconds.internal_error(500) andservice_unavailable(503): try again later.
Written for a person. May change; do not parse it.
object
The failing field, dot separated, such as prize.label.
object
Example
{ "error": { "code": "not_found", "message": "Channel 0192f3a1-0000-7000-8000-00000000c009 not found." }}