API

Live DonutSMP data as JSON.

Getting started

  1. 1Sign up
  2. 2Add Litecoin and buy the plan
  3. 3Create a key
curl https://api.donutapi.net/v1/balance/Steve \
  -H "X-API-Key: donut_your_key"

Conventions

Base URL
https://api.donutapi.net/v1
Auth
X-API-Key header or Authorization: Bearer
Numbers
Rounded like the game shows them. display has the original text.
Times
Unix seconds
Errors
{ "error": "..." } with a 4xx or 5xx status
Rate limit
Per key, per minute. See X-RateLimit-Remaining.
Cache
Repeat lookups are cached for a short time (cached: true).

Errors

400
Not a valid username
401
Missing or wrong key
402
Your plan has run out
404
Player not found, or no watch set
429
Too many requests, wait a minute
503
Busy or too slow, try again

Players

GET/balance/{username}

A player's money. Bedrock players work with or without the dot in front of their name.

curl https://api.donutapi.net/v1/balance/Steve \
  -H "X-API-Key: donut_your_key"
{
  "username": "Steve",
  "balance": 1500000,
  "display": "1.5M",
  "fetchedAt": 1791234567,
  "cached": false
}

Stats

GET/stats/{username}

A player's stats. Playtime is in seconds. display has each value as the game shows it.

curl https://api.donutapi.net/v1/stats/Steve \
  -H "X-API-Key: donut_your_key"
{
  "username": "Steve",
  "stats": {
    "money": 17800,
    "shards": 121,
    "kills": 610,
    "deaths": 652,
    "playtime": 3816000,
    "blocksPlaced": 330000,
    "blocksBroken": 343000
  },
  "display": {
    "money": "17.8K",
    "playtime": "44d 4h"
  },
  "fetchedAt": 1791234567,
  "cached": false
}

Payments

PUT/watch

Watch a player, usually your own shop account. One at a time; sending another name switches to it. webhookUrl is optional.

{ "username": "YourShop", "webhookUrl": "https://your-site.com/donut-payments" }
GET/watch

Your watch. status is starting (takes a few minutes), active, not_found (never joined DonutSMP) or paused (plan ran out).

{
  "username": "YourShop",
  "status": "active",
  "watchingSince": 1791234000,
  "webhookUrl": "https://your-site.com/donut-payments",
  "webhookSecret": "whsec_...",
  "createdAt": 1791233800
}
GET/payments/confirm?from={username}&amount={n}&since={unix}

Did this player pay you since this time? amount is optional and matched the way the game rounds (1.5K). If paid is false and certain is false, we were not watching the whole time, so check again later.

curl https://api.donutapi.net/v1/payments/confirm?from=Steve&amount=1500&since=1791234000 \
  -H "X-API-Key: donut_your_key"
{
  "paid": true,
  "certain": true,
  "payment": { "id": 42, "from": "Steve", "to": "YourShop", "amount": 1500, "display": "1.5K", "at": 1791234567 },
  "status": "active",
  "watchingSince": 1791234000
}
GET/payments?from={username}&page={n}

Payments to or from the player you watch, newest first, 10 per page. from is optional.

{
  "items": [{ "id": 42, "from": "Steve", "to": "YourShop", "amount": 1500, "display": "1.5K", "at": 1791234567 }],
  "page": 1, "pageSize": 10, "total": 1
}
DELETE/payments

Clears the payments list. New payments keep coming in, and confirm still checks the cleared ones.

POST/watch/test-webhook

Sends a test payment to your webhook now and says whether your server answered OK.

{ "ok": true, "error": null }
POST/watch/secret

Makes a new webhook signing secret. The old one stops working at once.

DELETE/watch

Stop watching.

Account

GET/me

When your plan ends and your rate limit.

{ "planUntil": 1793826567, "rateLimit": 60 }

Webhooks

If your watch has a webhook URL, every payment is sent there as a POST. Check the signature before you trust it.

Signature
X-Donutapi-Signature: HMAC-SHA256 of {X-Donutapi-Timestamp}.{body} with your signing secret
Secret
On your Payment watch page, or in GET /watch
Answer
Reply with any 2xx status within 10 seconds
Retries
Up to 8 times over about two hours
Test
The Send a test button, or POST /watch/test-webhook, sends event test
{
  "event": "payment",
  "watching": "YourShop",
  "id": 42,
  "from": "Steve",
  "to": "YourShop",
  "amount": 1500,
  "display": "1.5K",
  "at": 1791234567
}
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verifyWebhook(rawBody, headers, secret) {
  const expected = 'sha256=' + createHmac('sha256', secret)
    .update(headers['x-donutapi-timestamp'] + '.' + rawBody)
    .digest('hex');
  const given = headers['x-donutapi-signature'] ?? '';
  return given.length === expected.length &&
    timingSafeEqual(Buffer.from(given), Buffer.from(expected));
}

Examples

Keep your key on your server, never in a browser.

const res = await fetch('https://api.donutapi.net/v1/stats/Steve', {
  headers: { 'X-API-Key': process.env.DONUTAPI_KEY },
});
if (!res.ok) throw new Error((await res.json()).error);
const { stats } = await res.json();