Run your ARK servers from your own website or Discord bot: give items, dinos and kits, hand out points, moderate players, talk in chat, and hear about what happens in game.
Everything is plain HTTPS and JSON. The examples below use curl and JavaScript, but any language works. The full list of endpoints is at the bottom of this page, and machine-readable at GET /v1/openapi.json.
Quick start
- Ask the account owner for a key. They make it in the app, under Permissions → API.
- Check it works:
curl https://www.control-ark.com/v1/me -H "Authorization: Bearer ark_k_xxxxxxxx"
- Find a player, then give them something:
curl "https://www.control-ark.com/v1/players?gamertag=SomeGuy123" -H "Authorization: Bearer $ARK_KEY"
curl -X POST https://www.control-ark.com/v1/players/2535400000000001/items \
-H "Authorization: Bearer $ARK_KEY" -H "Content-Type: application/json" \
-d '{"items":[{"blueprint":"/Game/PrimalEarth/CoreBlueprints/Resources/PrimalItemResource_MetalIngot.PrimalItemResource_MetalIngot","quantity":500}]}'
That's it. The player gets the ingots and a chat message saying they arrived.
Getting a key
The account owner makes keys in the app, under Permissions → API. Each key has:
- What it can do: deliveries, points, player actions, moderation and so on. Tick only what your code needs.
- Servers: all of the account's servers, or some of them.
- IP addresses (optional, under Advanced): where calls must come from. Use this whenever your bot runs on a fixed server.
Everything else (which player, which dino, its level and stats, whether a kit costs points) goes in each call.
The key (ark_k_…) is shown once. Keep it on your server, never in a web page or a public repository: anyone who has it can do what it allows.
A key can't teleport anyone, use spectator, move or control the bots, or send console commands of its own.
How calls work
Send the key with every call:
Authorization: Bearer ark_k_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Bodies are JSON, sent with Content-Type: application/json.
Players are named by XUID
Everything you do to a player goes to /v1/players/{xuid}/…. You only need their XUID: the API finds which of your servers they're on, and their character there, by itself.
Don't know the XUID? Search for them:
GET /v1/players?gamertag=SomeGuy123
GET /v1/players?name=rex (gamertag or character name contains "rex")
GET /v1/players?tribe=Herd&online=true
Each player comes back once, with a row for every server they've played on (each server has its own character):
{
"xuid": "2535400000000001",
"gamertag": "SomeGuy123",
"firstSeen": "2026-10-01T18:22:04.000Z",
"lastSeen": "2026-10-29T14:03:11.000Z",
"online": true,
"servers": [
{ "serverId": "cm2…", "server": "PvE Island", "name": "Rexy", "implant": 123456789,
"tribe": "The Herd", "tribeId": 1234567890, "online": true, "lastSeen": "2026-10-29T14:03:11.000Z" }
]
}
The filters are server, online, xuid, implant, gamertag (exact, any case), name, tribe, tribeId and page. Use any number of them together.
In-game actions need the player to be on right now. If they aren't, you get 409 player_offline. If someone is on more than one of your servers and you want a particular one, add "server": "<server id>" to the body. Your server ids are in GET /v1/servers.
Answers
| Status | Meaning |
|---|---|
| 200 | Done. Items, XP, chat, moderation and points happen straight away. |
| 202 | On its way. Dino deliveries, colour and imprint are done by the bot one at a time. Follow them with GET /v1/jobs/{id} or the delivery.done and delivery.failed webhooks. |
| 502 | The bot went offline before it could be sent. Nothing was done, and any points taken were given back. |
Every action answers with an id. Keep it if you want to check on the call later.
Every refusal looks like this:
{ "error": { "code": "player_offline", "message": "that player isn't on any of this key's servers right now" } }
| Code | Meaning |
|---|---|
unauthorized | Missing, wrong, turned-off or revoked key. |
missing_scope | The key isn't allowed to do this. |
ip_not_allowed | The call came from an IP address the key doesn't allow. |
bad_request | The body or query isn't right. issues says what. |
server_not_found / player_not_found | Not on this key's servers. |
player_offline | The player isn't on right now. |
bot_unavailable | No bot is in the game on that server. |
rate_limited / daily_limit / cooldown / queue_full | Slow down. Check Retry-After. |
not_enough_points, kit_refused | The player can't pay, or the kit's rules say no. |
blocked_dino | Bosses and titans are never delivered. |
Limits
Each key may make 120 reads and 30 in-game actions a minute. Every answer has X-RateLimit-Limit and X-RateLimit-Remaining. Up to 50 deliveries per key can be on their way at once. Calls that can't be undone have a daily cap per key: 20 inventory clears, 5 tribe destroys and 6 wild dino wipes.
Retrying safely
Send Idempotency-Key: <something unique> with every action, such as your order number or the Discord message id. If you send the same key again (after a timeout, say), you get the first answer back with Idempotency-Replay: true, and nothing is done twice. A refused call (player_offline, for example) isn't stored, so you can retry it with the same key later.
Examples
Items
POST /v1/players/2535400000000001/items
{ "items": [
{ "blueprint": "/Game/PrimalEarth/CoreBlueprints/Resources/PrimalItemResource_MetalIngot.PrimalItemResource_MetalIngot",
"quantity": 1000, "stackSize": 200 },
{ "blueprint": "/Game/PrimalEarth/CoreBlueprints/Weapons/PrimalItem_WeaponRifle.PrimalItem_WeaponRifle",
"quality": 10, "asBlueprint": true },
{ "itemId": 121, "quantity": 2 }
] }
| Field | Meaning |
|---|---|
blueprint or itemId | The item, by blueprint path or by its item number. |
quantity | How many (default 1). |
quality | 0 is primitive, higher is better (default 0). |
asBlueprint | Give the blueprint instead of the item. |
stackSize | Split big amounts into stacks of this size. |
Dinos
A level 150 Rex in a cryopod:
POST /v1/players/2535400000000001/dinos
{ "method": "cryo",
"dinos": [{ "blueprint": "/Game/PrimalEarth/Dinos/Rex/Rex_Character_BP.Rex_Character_BP", "level": 150 }] }
An Argentavis with exact stats and colours, and a breeding pair of Ankylos, dropped next to the player:
POST /v1/players/2535400000000001/dinos
{ "method": "unclaim",
"dinos": [
{ "blueprint": "/Game/PrimalEarth/Dinos/Argentavis/Argent_Character_BP.Argent_Character_BP",
"wildPoints": [40, 40, 0, 40, 40, 0, 0, 0], "tamedPoints": [0, 0, 0, 0, 0, 0, 0, 0],
"colors": [12, 0, 0, 55, 0, 0] },
{ "blueprint": "/Game/PrimalEarth/Dinos/Ankylo/Ankylo_Character_BP.Ankylo_Character_BP", "level": 120, "pair": true }
] }
| Field | Meaning |
|---|---|
level | Spread evenly over the stats. |
wildPoints | 8 stats (health, stamina, oxygen, food, weight, melee, speed, crafting), each 0 to 255. Use these instead of level for exact stats. |
tamedPoints | The same 8 stats, levels put in after taming. |
colors | 6 colour regions. |
count | How many of this dino. |
pair | A male and a female. |
method is cryo (in a cryopod, handed to the player) or unclaim (the bot stands beside the player, spawns the dino there and unclaims it so they can claim it). Up to 10 dinos per call. The answer is 202, so follow it:
GET /v1/jobs/cm2abc…
{ "id": "cm2abc…", "action": "deliver.dinos", "status": "done", "dinos": { "done": 2, "total": 2 }, … }
Kits
GET /v1/kits
[{ "name": "starter", "cost": 100, "items": [{ "name": "Metal Pick", "blueprint": "…", "quantity": 1 }], "dinos": [] }]
POST /v1/players/2535400000000001/kit
{ "kit": "starter", "chargePoints": true }
chargePoints (default false) takes the kit's cost from the player's points. ignoreRules: true skips the kit's own limits and cooldowns.
Several things at once
batch runs up to 10 commands for one player, in order. Each one is checked and counted as its own call, so the key needs every scope they use.
POST /v1/players/2535400000000001/batch
{ "commands": [
{ "command": "kit", "kit": "vip" },
{ "command": "points.add", "amount": 1000, "reason": "VIP purchase" },
{ "command": "message", "message": "Thanks for supporting the server!" }
],
"stopOnError": true }
The answer has one result per command:
{ "xuid": "2535400000000001", "results": [
{ "command": "kit", "status": 200, "answer": { "id": "…", "status": "done", "kit": "vip", … } },
{ "command": "points.add", "status": 200, "answer": { "balance": 1500, … } },
{ "command": "message", "status": 200, "answer": { … } }
] }
With stopOnError, the commands after a refusal are skipped.
Player actions
POST /v1/players/2535400000000001/experience { "amount": 50000 }
POST /v1/players/2535400000000001/tek-engrams {}
POST /v1/players/2535400000000001/ascension { "type": "boss", "boss": "Manticore", "difficulty": "alpha" }
POST /v1/players/2535400000000001/kill {}
POST /v1/players/2535400000000001/mindwipe {}
POST /v1/players/2535400000000001/tribe { "tribeId": 1234567890 }
POST /v1/players/2535400000000001/color { "colors": { "0": 12, "3": 55 } }
POST /v1/players/2535400000000001/imprint { "anyTamed": false }
For color and imprint, the bot goes to the player and works on their tribe's nearest dino (for imprint, the nearest baby). Ask the player to stand next to it.
Moderation
POST /v1/players/2535400000000001/kick { "reason": "AFK" }
POST /v1/players/2535400000000001/ban { "reason": "cheating" }
DELETE /v1/players/2535400000000001/ban
POST /v1/players/2535400000000001/whitelist
Bans and the whitelist work whether the player is on or not, on every one of the key's servers (or only on server, if you give one).
Points from a Discord bot
const ARK = "https://www.control-ark.com/v1";
const headers = { authorization: `Bearer ${process.env.ARK_KEY}`, "content-type": "application/json" };
// /vote reward: 500 points, once per Discord interaction even if it's retried.
const res = await fetch(`${ARK}/players/${xuid}/points/add`, {
method: "POST",
headers: { ...headers, "idempotency-key": interaction.id },
body: JSON.stringify({ amount: 500, reason: "Voted" }),
});
const out = await res.json();
await interaction.reply(res.ok ? `New balance: ${out.balance}` : `Couldn't: ${out.error.message}`);
A shop order on your website
// Give the order, wait for the dinos, then mark it paid out.
const order = await fetch(`${ARK}/players/${xuid}/dinos`, {
method: "POST",
headers: { ...headers, "idempotency-key": `order-${orderId}` },
body: JSON.stringify({ method: "cryo", dinos: [{ blueprint: REX, level: 150 }] }),
}).then((r) => r.json());
if (order.error) throw new Error(order.error.message);
let job = order;
while (job.status === "running") {
await new Promise((r) => setTimeout(r, 3000));
job = await fetch(`${ARK}/jobs/${order.id}`, { headers }).then((r) => r.json());
}
console.log(job.status === "done" ? "delivered" : `failed: ${job.detail}`);
Server messages
POST /v1/servers/{id}/broadcast { "message": "Double XP this weekend!" }
POST /v1/tribes/1234567890/message { "message": "Your vault is open" }
Webhooks
To hear about events, add a webhook with POST /v1/webhooks (scope webhooks), or have the owner add one in the app. The answer includes a secret (whsec_…) that is shown once.
| Event | When |
|---|---|
player.joined, player.left | Someone joins or leaves one of the key's servers. |
chat.message | Every chat line. Needs the webhooks.chat scope. |
kit.used | An in-game /kit claim finished. Needs webhooks.kits. |
command.used | Another in-game command finished (/suicide, /tp, /color, …). |
delivery.done, delivery.failed | One of this key's deliveries finished. |
points.changed | Points were given or taken. Playtime earned each minute isn't sent. |
tracker.osd, tracker.boss | An Orbital Supply Drop spawned, or a boss timer changed. |
Each event is a POST like this:
{ "id": "evt_…", "type": "delivery.done", "createdAt": "2026-10-29T14:03:11.000Z", "serverId": "…", "data": { … } }
Answer with any 2xx within 5 seconds. A failed post is tried again after 5 and then 30 seconds. A webhook that fails 20 times in a row is paused until it's resumed (PATCH /v1/webhooks/{id} with { "disabled": false }).
Checking the signature
Every post has X-Ark-Signature: t=<unix seconds>,v1=<hex>, where hex is the HMAC-SHA256 of <t>.<raw body> keyed with your secret. Check it, and turn away posts whose t is more than 5 minutes old:
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(rawBody, header, secret) {
const [, t, sig] = /^t=(\d+),v1=([0-9a-f]+)$/.exec(header ?? "") ?? [];
if (!t || Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const want = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
const got = Buffer.from(sig, "hex");
return got.length === want.length && timingSafeEqual(got, want);
}
Webhook addresses must be https and can't point at private or local networks.
Every endpoint
The scope on the right is what the key needs. The same list, machine-readable, is at /v1/openapi.json.
Your key and servers
/v1/meThis key: its name, scopes, servers and limits.
/v1/serversThe key's servers: map, Nitrado status, bot status (ready, starting, offline, none) and how many players are on.
/v1/servers/{id}One server in detail: everything in the list, plus who's on now (with players.read), how many players and tribes it has seen, its bots, the last Orbital Supply Drop and the boss timers.
Players
/v1/playersplayers.readPlayers seen on the key's servers, online first, 50 a page. Filters (combine any): server, online=true|false, xuid, implant, gamertag (exact, any case), name (gamertag or character name contains), tribe (name contains), tribeId, page. Each player has a row per server they've played on.
/v1/players/{xuid}players.readOne player, with a row per server (character name, implant, tribe, tribeId, online, last seen), and points with points.read.
Giving things
/v1/players/{xuid}/itemsdeliver.itemsGive items. By blueprint, or by itemId (GiveItemNumToPlayer's number). stackSize splits big amounts into stacks.
{
"items": [
{
"blueprint": "/Game/PrimalEarth/CoreBlueprints/Resources/PrimalItemResource_MetalIngot.PrimalItemResource_MetalIngot",
"quantity": 500,
"quality": 0,
"asBlueprint": false,
"stackSize": 100
},
{
"itemId": 121,
"quantity": 1
}
]
}/v1/players/{xuid}/dinosdeliver.dinosDeliver up to 10 dinos: method cryo (in a cryopod) or unclaim (dropped beside them). Give level, or wildPoints and tamedPoints (8 stats each, 0-255). Bosses and titans are refused. Answers 202: follow it with GET /jobs/{id}.
{
"method": "cryo",
"dinos": [
{
"blueprint": "/Game/PrimalEarth/Dinos/Rex/Rex_Character_BP.Rex_Character_BP",
"level": 150,
"count": 1,
"pair": false,
"colors": [
0,
0,
0,
0,
0,
0
]
}
]
}/v1/kitsdeliver.kitsThe account's kits and what's in them (item and dino names, blueprints, amounts).
/v1/players/{xuid}/kitdeliver.kitsGive a kit. chargePoints (default false) takes its points cost; ignoreRules skips its limits and cooldowns.
{
"kit": "starter",
"chargePoints": false,
"ignoreRules": false
}Player actions
/v1/players/{xuid}/experienceplayers.manageGive XP.
{
"amount": 50000,
"shareWithTribe": false
}/v1/players/{xuid}/tek-engramsplayers.manageUnlock Tek engrams (engram: one engram, or leave it out).
/v1/players/{xuid}/ascensionplayers.managetype max (MaxAscend), all-bosses (DefeatAllBosses) or boss (DefeatBoss, with boss and difficulty gamma|beta|alpha).
{
"type": "boss",
"boss": "Manticore",
"difficulty": "alpha"
}/v1/players/{xuid}/killplayers.manageKill the player's character.
/v1/players/{xuid}/cryopodplayers.manageGive an empty cryopod.
/v1/players/{xuid}/mindwipeplayers.manageGive a Mindwipe Tonic.
/v1/players/{xuid}/colordinos.manageThe bot goes to the player and recolours their tribe's nearest dino (regions 0-5, colours 1-255). Answers 202.
{
"colors": {
"0": 12,
"3": 55
}
}/v1/players/{xuid}/imprintdinos.manageThe bot goes to the player and imprints their tribe's nearest baby to 100% (anyTamed: grown dinos too). Answers 202.
{
"anyTamed": false
}/v1/players/{xuid}/clear-inventoryplayers.destructiveClear the player's inventory. 20 a day per key.
{
"inventory": true,
"slotItems": true,
"equipped": false
}/v1/players/{xuid}/creativeplayers.creativeToggle creative mode for the player.
/v1/players/{xuid}/messageserver.chatA private chat message to the player.
{
"message": "Thanks for voting!"
}/v1/players/{xuid}/tribetribes.managePut the player in a tribe: tribeId, or tribe (its name).
{
"tribeId": 1234567890
}/v1/players/{xuid}/batchUp to 10 of the commands above for one player in one call, run in order. Each needs its own scope and counts as its own call. Commands: items, dinos, kit, experience, tek-engrams, ascension, kill, cryopod, mindwipe, clear-inventory, creative, message, tribe, color, imprint, kick, rename, ban, unban, whitelist, unwhitelist, points.add, points.remove. stopOnError skips the rest after a refusal.
{
"commands": [
{
"command": "items",
"items": [
{
"blueprint": "/Game/PrimalEarth/CoreBlueprints/Resources/PrimalItemResource_MetalIngot.PrimalItemResource_MetalIngot",
"quantity": 100
}
]
},
{
"command": "experience",
"amount": 10000
},
{
"command": "message",
"message": "Your order is here!"
}
],
"stopOnError": false
}Moderation
/v1/players/{xuid}/kickmoderationKick the player.
{
"reason": "AFK"
}/v1/players/{xuid}/renamemoderationRename the player's character.
{
"name": "Survivor"
}/v1/players/{xuid}/banmoderationBan them (on or not) on every key server with a bot in the game, or only on server.
{
"reason": "cheating"
}/v1/players/{xuid}/banmoderationUnban.
/v1/players/{xuid}/whitelistmoderationLet them join (AllowPlayerToJoinNoCheck).
/v1/players/{xuid}/whitelistmoderationTake them off the whitelist.
Points
/v1/players/{xuid}/pointspoints.readBalance, rank and playtime.
/v1/players/{xuid}/points/ledgerpoints.readThe player's points history, newest first. Query: page.
/v1/players/{xuid}/points/addpoints.writeGive points.
{
"amount": 500,
"reason": "Voted"
}/v1/players/{xuid}/points/removepoints.writeTake points (allowNegative to go below 0).
{
"amount": 500,
"reason": "Shop purchase"
}/v1/points/transferpoints.writeMove points between two players.
{
"from": "2535400000000001",
"to": "2535400000000002",
"amount": 100
}/v1/leaderboards/pointspoints.readTop balances. Query: limit.
/v1/leaderboards/playtimepoints.readMost played. Query: period=all|month|week|day, limit.
Servers
/v1/servers/{id}/chatserver.chatA line in server chat.
{
"message": "Restart in 10 minutes"
}/v1/servers/{id}/broadcastserver.chatA broadcast across everyone's screen.
{
"message": "Double XP weekend!"
}/v1/servers/{id}/motdserver.chatSet the message of the day.
{
"message": "Welcome to the server"
}/v1/servers/{id}/timeserver.manageSet the time of day.
{
"time": "08:00"
}/v1/servers/{id}/saveserver.manageSave the world (once per 10 minutes per server).
/v1/servers/{id}/reload-configserver.manageReload the server's dynamic config.
/v1/servers/{id}/wipe-wild-dinosserver.manageKill every wild dino (once an hour per server, 6 a day per key).
Tribes
/v1/tribes/{tribeId}/messageserver.chatA message to a tribe. The server is found from where its members have been seen (give server if it's on several).
{
"message": "Your vault is open"
}/v1/tribes/renametribes.manageRename a tribe (by its name).
{
"tribe": "Old Name",
"newName": "New Name"
}/v1/tribes/mergetribes.managePut two players (on now, on the same server) in a new tribe.
{
"players": [
"2535400000000001",
"2535400000000002"
],
"tribe": "Allies"
}/v1/tribes/{tribeId}/destroytribes.destroyDestroy a tribe or part of it. Always name the server; confirm repeats the tribe id. 5 a day per key.
{
"server": "server id",
"what": "structures",
"confirm": 1234567890
}Following your calls
/v1/jobs/{id}One call made with this key (its id is in every answer) and how far it's got: status, dinos done of total, detail.
/v1/jobsThis key's recent calls, newest first. Query: status=running|done|failed, xuid, action, limit.
Giveaways
/v1/giveawaysevents.runThe giveaways the key may start.
/v1/giveaways/{id}/runevents.runStart a giveaway now.
Webhooks
/v1/webhookswebhooksThis key's webhooks.
/v1/webhookswebhooksAdd a webhook (https). Answers its secret once. Events: player.joined, player.left, chat.message, kit.used, command.used, delivery.done, delivery.failed, points.changed, tracker.osd, tracker.boss.
{
"url": "https://example.com/ark-events",
"events": [
"player.joined",
"delivery.done"
],
"serverIds": null
}/v1/webhooks/{id}webhooksPause or resume it.
{
"disabled": false
}/v1/webhooks/{id}webhooksRemove it.
/v1/webhooks/{id}/testwebhooksSend it a test event.