ControlARK Get a bot on Discord

API docs

For your own Discord bot, website or tools. Machine-readable at /v1/openapi.json.

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

  1. Ask the account owner for a key. They make it in the app, under Permissions → API.
  2. Check it works:
curl https://www.control-ark.com/v1/me -H "Authorization: Bearer ark_k_xxxxxxxx"
  1. 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

StatusMeaning
200Done. Items, XP, chat, moderation and points happen straight away.
202On 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.
502The 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" } }
CodeMeaning
unauthorizedMissing, wrong, turned-off or revoked key.
missing_scopeThe key isn't allowed to do this.
ip_not_allowedThe call came from an IP address the key doesn't allow.
bad_requestThe body or query isn't right. issues says what.
server_not_found / player_not_foundNot on this key's servers.
player_offlineThe player isn't on right now.
bot_unavailableNo bot is in the game on that server.
rate_limited / daily_limit / cooldown / queue_fullSlow down. Check Retry-After.
not_enough_points, kit_refusedThe player can't pay, or the kit's rules say no.
blocked_dinoBosses 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 }
] }
FieldMeaning
blueprint or itemIdThe item, by blueprint path or by its item number.
quantityHow many (default 1).
quality0 is primitive, higher is better (default 0).
asBlueprintGive the blueprint instead of the item.
stackSizeSplit 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 }
  ] }
FieldMeaning
levelSpread evenly over the stats.
wildPoints8 stats (health, stamina, oxygen, food, weight, melee, speed, crafting), each 0 to 255. Use these instead of level for exact stats.
tamedPointsThe same 8 stats, levels put in after taming.
colors6 colour regions.
countHow many of this dino.
pairA 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.

EventWhen
player.joined, player.leftSomeone joins or leaves one of the key's servers.
chat.messageEvery chat line. Needs the webhooks.chat scope.
kit.usedAn in-game /kit claim finished. Needs webhooks.kits.
command.usedAnother in-game command finished (/suicide, /tp, /color, …).
delivery.done, delivery.failedOne of this key's deliveries finished.
points.changedPoints were given or taken. Playtime earned each minute isn't sent.
tracker.osd, tracker.bossAn 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

GET/v1/me

This key: its name, scopes, servers and limits.

GET/v1/servers

The key's servers: map, Nitrado status, bot status (ready, starting, offline, none) and how many players are on.

GET/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

GET/v1/playersplayers.read

Players 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.

GET/v1/players/{xuid}players.read

One player, with a row per server (character name, implant, tribe, tribeId, online, last seen), and points with points.read.

Giving things

POST/v1/players/{xuid}/itemsdeliver.items

Give 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
    }
  ]
}
POST/v1/players/{xuid}/dinosdeliver.dinos

Deliver 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
      ]
    }
  ]
}
GET/v1/kitsdeliver.kits

The account's kits and what's in them (item and dino names, blueprints, amounts).

POST/v1/players/{xuid}/kitdeliver.kits

Give a kit. chargePoints (default false) takes its points cost; ignoreRules skips its limits and cooldowns.

{
  "kit": "starter",
  "chargePoints": false,
  "ignoreRules": false
}

Player actions

POST/v1/players/{xuid}/experienceplayers.manage

Give XP.

{
  "amount": 50000,
  "shareWithTribe": false
}
POST/v1/players/{xuid}/tek-engramsplayers.manage

Unlock Tek engrams (engram: one engram, or leave it out).

POST/v1/players/{xuid}/ascensionplayers.manage

type max (MaxAscend), all-bosses (DefeatAllBosses) or boss (DefeatBoss, with boss and difficulty gamma|beta|alpha).

{
  "type": "boss",
  "boss": "Manticore",
  "difficulty": "alpha"
}
POST/v1/players/{xuid}/killplayers.manage

Kill the player's character.

POST/v1/players/{xuid}/cryopodplayers.manage

Give an empty cryopod.

POST/v1/players/{xuid}/mindwipeplayers.manage

Give a Mindwipe Tonic.

POST/v1/players/{xuid}/colordinos.manage

The 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
  }
}
POST/v1/players/{xuid}/imprintdinos.manage

The bot goes to the player and imprints their tribe's nearest baby to 100% (anyTamed: grown dinos too). Answers 202.

{
  "anyTamed": false
}
POST/v1/players/{xuid}/clear-inventoryplayers.destructive

Clear the player's inventory. 20 a day per key.

{
  "inventory": true,
  "slotItems": true,
  "equipped": false
}
POST/v1/players/{xuid}/creativeplayers.creative

Toggle creative mode for the player.

POST/v1/players/{xuid}/messageserver.chat

A private chat message to the player.

{
  "message": "Thanks for voting!"
}
POST/v1/players/{xuid}/tribetribes.manage

Put the player in a tribe: tribeId, or tribe (its name).

{
  "tribeId": 1234567890
}
POST/v1/players/{xuid}/batch

Up 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

POST/v1/players/{xuid}/kickmoderation

Kick the player.

{
  "reason": "AFK"
}
POST/v1/players/{xuid}/renamemoderation

Rename the player's character.

{
  "name": "Survivor"
}
POST/v1/players/{xuid}/banmoderation

Ban them (on or not) on every key server with a bot in the game, or only on server.

{
  "reason": "cheating"
}
DELETE/v1/players/{xuid}/banmoderation

Unban.

POST/v1/players/{xuid}/whitelistmoderation

Let them join (AllowPlayerToJoinNoCheck).

DELETE/v1/players/{xuid}/whitelistmoderation

Take them off the whitelist.

Points

GET/v1/players/{xuid}/pointspoints.read

Balance, rank and playtime.

GET/v1/players/{xuid}/points/ledgerpoints.read

The player's points history, newest first. Query: page.

POST/v1/players/{xuid}/points/addpoints.write

Give points.

{
  "amount": 500,
  "reason": "Voted"
}
POST/v1/players/{xuid}/points/removepoints.write

Take points (allowNegative to go below 0).

{
  "amount": 500,
  "reason": "Shop purchase"
}
POST/v1/points/transferpoints.write

Move points between two players.

{
  "from": "2535400000000001",
  "to": "2535400000000002",
  "amount": 100
}
GET/v1/leaderboards/pointspoints.read

Top balances. Query: limit.

GET/v1/leaderboards/playtimepoints.read

Most played. Query: period=all|month|week|day, limit.

Servers

POST/v1/servers/{id}/chatserver.chat

A line in server chat.

{
  "message": "Restart in 10 minutes"
}
POST/v1/servers/{id}/broadcastserver.chat

A broadcast across everyone's screen.

{
  "message": "Double XP weekend!"
}
PUT/v1/servers/{id}/motdserver.chat

Set the message of the day.

{
  "message": "Welcome to the server"
}
POST/v1/servers/{id}/timeserver.manage

Set the time of day.

{
  "time": "08:00"
}
POST/v1/servers/{id}/saveserver.manage

Save the world (once per 10 minutes per server).

POST/v1/servers/{id}/reload-configserver.manage

Reload the server's dynamic config.

POST/v1/servers/{id}/wipe-wild-dinosserver.manage

Kill every wild dino (once an hour per server, 6 a day per key).

Tribes

POST/v1/tribes/{tribeId}/messageserver.chat

A 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"
}
POST/v1/tribes/renametribes.manage

Rename a tribe (by its name).

{
  "tribe": "Old Name",
  "newName": "New Name"
}
POST/v1/tribes/mergetribes.manage

Put two players (on now, on the same server) in a new tribe.

{
  "players": [
    "2535400000000001",
    "2535400000000002"
  ],
  "tribe": "Allies"
}
POST/v1/tribes/{tribeId}/destroytribes.destroy

Destroy 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

GET/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.

GET/v1/jobs

This key's recent calls, newest first. Query: status=running|done|failed, xuid, action, limit.

Giveaways

GET/v1/giveawaysevents.run

The giveaways the key may start.

POST/v1/giveaways/{id}/runevents.run

Start a giveaway now.

Webhooks

GET/v1/webhookswebhooks

This key's webhooks.

POST/v1/webhookswebhooks

Add 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
}
PATCH/v1/webhooks/{id}webhooks

Pause or resume it.

{
  "disabled": false
}
DELETE/v1/webhooks/{id}webhooks

Remove it.

POST/v1/webhooks/{id}/testwebhooks

Send it a test event.