Shiba Bank API v1.0
Credit Stars from your wallet in @barboskich_stars_bot to people's wallets straight from your own service: rewards for actions, prizes, payouts to moderators. The Stars never leave the bot: they move from wallet to wallet, like the +send command.
Base URL: https://shiba-bank.com/api/v1
Quick start
- Open @barboskich_stars_bot, send
/apiand press “🔑 Create key”. The key is shown once: store it in theSHIBA_API_KEYenvironment variable on your server. - Top up your wallet in the bot: transfers are paid from your balance only.
- Check the key and the balance:
curl https://shiba-bank.com/api/v1/balance -H "Authorization: Bearer $SHIBA_API_KEY" - Send Stars:
Answer:curl -X POST https://shiba-bank.com/api/v1/transfers \ -H "Authorization: Bearer $SHIBA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"telegram_id": 123456789, "amount": 5, "comment": "за модерацию никнейма", "notify": true, "idempotency_key": "moderation-8841"}'{ "success": true, "end_balance": 307, "replayed": false, "transfer": { "id": "tr_5f0c2a9e41b7d3c8a6e1f024", "status": "completed", "telegram_id": 123456789, "amount": 5, "comment": "за модерацию никнейма", "notify": true, "idempotency_key": "moderation-8841", "error": null, "end_balance": 307, "created_at": "2026-09-23T14:13:05.120931Z" } }
The bot messages the recipient with the amount, the sender and your comment unless notify is false.
Basics
- HTTPS only. Requests and answers are UTF-8 JSON.
- Every answer has
success:trueorfalse. Withfalsecomes anerrorobject with a stablecode(branch on it in your code), a humanmessageand details, plus arequest_id. - Every answer carries an
X-Request-Idheader: quote it when you contact support. - Amounts are whole Stars. Times are ISO 8601 in UTC, ending in
Z. - Numbers may also be sent as a digit string (
"5"); booleans only astrue/false. - Unknown fields are refused (
unknown_field), so a typo never becomes a silently ignored parameter. - The version is in the path,
/api/v1. Within v1 fields are only added; nothing is removed or changes meaning.
| Header | When | Value |
|---|---|---|
Authorization | always | Bearer sbk_… |
Content-Type | POST | application/json |
Idempotency-Key | POST /transfers, optional instead of the field | same as idempotency_key |
Keys and authentication
Every request except /status carries the key in a header:
Authorization: Bearer sbk_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
X-Api-Key: sbk_… works too. A key in the URL (?key=) is refused and should be treated as exposed.
- A key is 256 random bits and starts with
sbk_. The bot keeps only its SHA-256 fingerprint and shows the key once. - An account has one key. “♻️ New key” in
/apiswitches the old one off at once; “🗑 Revoke” switches it off without a replacement. Limits and addresses are kept. - A key acts for its owner: their wallet pays, and the recipient sees them as the sender.
Security
What a key can do: read your balance, limits and its own transfers; check whether a person can receive Stars; send Stars from your wallet to people's wallets in the bot.
What a key can never do: withdraw Stars from the bot, sell or buy them, charge anybody else's wallet, change its own limits or addresses, see other people's data. Settings change only inside the bot.
How to protect the key
- Keep the key on your server only: in an environment variable or a secret store. Never in a repository, in client code, a mini app or a website: whatever reaches a browser or a phone can be read.
- The API answers servers only: there are no CORS headers, so no web page can use your key from a browser.
- Put your server's IP in
/api→ “🌐 Addresses”: the key stops working from anywhere else. - Set the limits to your real load: the daily limit is the most a leaked key can cost you.
- Keep in the wallet what the next payouts need, not everything.
- Suspect a leak? Press “♻️ New key” or “🗑 Revoke” at once. The old key stops working that very second.
What the server does
- Every transfer goes the way
+sendgoes: anti-fraud, freezes and bans apply the same way. - One key's transfers run strictly one at a time and limits are counted atomically: parallel requests cannot slip past the daily limit.
- An address sending many wrong keys is refused for a while before any key is checked.
- The comment is cleaned of invisible and control characters and cannot contain links: the recipient reads it as your words, not the bot's.
Limits
| What | How much | Where to change |
|---|---|---|
| One transfer | default 500 ⭐, at most 100,000 ⭐ | /api → 📏 |
| A day (UTC) | default 2000 ⭐, at most 1,000,000 ⭐ | /api → 📅 |
| Requests per key | 10 per second, bursts up to 30 | — |
| Transfers per key | 3 per second, bursts up to 10; one at a time | — |
| Requests per IP | 10 per second, bursts up to 40 (beyond that, HTTP 429 from the web server) | — |
| Request body | 4096 bytes | — |
| Allowed addresses | up to 10 IPs or networks | /api → 🌐 |
The daily limit follows the UTC day (it resets at 00:00 UTC) and includes every completed or started transfer of the key. Current figures are in GET /me.
With 429 and 503 read the Retry-After header: that many seconds before a retry.
Idempotency and retries
Networks fail. If the answer to a transfer never arrived, you do not know whether the Stars moved. So every transfer carries your idempotency key, a stable name of the operation: moderation-8841, order-1234-reward, daily-2026-09-23-user-42. Repeat the request with the same key as often as you like: the Stars move once.
| Situation | What the API answers |
|---|---|
| A new key | Makes the transfer. |
| Same key, same fields, completed | The same answer, replayed: true, nothing charged again. |
| Same key, same fields, pending | Finishes that same transfer. |
| Same key, different fields | 409 idempotency_conflict. |
| Same key after a refusal (balance, limit, frozen) | Tries again: a refusal moved nothing and did not take the key. |
The rule is simple: derive the key from your data, never a fresh random one per attempt. A random key per attempt switches the double-payment protection off.
Safe to repeat: a network error, a timeout, HTTP 429, 500, 503. Do not repeat unchanged: other 4xx answers will not change by repetition.
| Status | Meaning |
|---|---|
completed | The Stars are with the recipient. Final. |
pending | Started, outcome not recorded yet (a dropped connection or a restart). Repeat the same request with the same key and it finishes. |
refused | Refused, nothing moved. The code is in error. |
POST /transfers — send Stars
Takes Stars from your wallet and credits them to the recipient's wallet in the bot.
| Field | Type | Required | Description |
|---|---|---|---|
telegram_id | integer | yes | The recipient's Telegram id. They must have started @barboskich_stars_bot at least once. |
amount | integer | yes | Stars to send: a whole number from 1 to 100,000, and within the key's limit. |
comment | string | no | The purpose of the payment, up to 255 characters. One line: line breaks and repeated spaces are joined. No links, domains, @mentions or hashtags. |
notify | boolean | no, true | Whether the bot messages the recipient about the credit. |
idempotency_key | string | yes* | Your unique operation key, 1–64 characters A–Z a–z 0–9 _ - . :. May be sent as the Idempotency-Key header. *Not needed with dry_run. |
dry_run | boolean | no, false | Check everything (key, recipient, limits, balance) and move nothing. |
Request
curl -X POST https://shiba-bank.com/api/v1/transfers \
-H "Authorization: Bearer $SHIBA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"telegram_id": 123456789, "amount": 5, "comment": "за модерацию никнейма", "notify": true, "idempotency_key": "moderation-8841"}'
Success — 200
{
"success": true,
"end_balance": 307,
"replayed": false,
"transfer": {
"id": "tr_5f0c2a9e41b7d3c8a6e1f024",
"status": "completed",
"telegram_id": 123456789,
"amount": 5,
"comment": "за модерацию никнейма",
"notify": true,
"idempotency_key": "moderation-8841",
"error": null,
"end_balance": 307,
"created_at": "2026-09-23T14:13:05.120931Z"
}
}
| Field | Description |
|---|---|
success | true: the Stars are credited. |
end_balance | Your wallet balance right after this transfer. |
replayed | true when this repeats a completed transfer: nothing new was charged. |
transfer.id | Our transfer id, tr_…. |
transfer.status | completed, pending or refused. |
transfer.error | The refusal code of a refused one, otherwise null. |
Checking without sending — dry_run
{
"telegram_id": 123456789,
"amount": 5,
"dry_run": true
}
{
"success": true,
"dry_run": true,
"end_balance": 307,
"balance": 312
}
Error — 422
{
"success": false,
"error": {
"code": "insufficient_balance",
"message": "Not enough Stars in the wallet.",
"balance": 3,
"required": 5
},
"request_id": "req_9b1e04c27d5a6f33"
}
GET /transfers/{id} — one transfer
A transfer's state by our id (tr_…). Only your account's transfers are visible.
curl https://shiba-bank.com/api/v1/transfers/tr_5f0c2a9e41b7d3c8a6e1f024 -H "Authorization: Bearer $SHIBA_API_KEY"
{
"success": true,
"transfer": {
"id": "tr_5f0c2a9e41b7d3c8a6e1f024",
"status": "completed",
"telegram_id": 123456789,
"amount": 5,
"comment": "за модерацию никнейма",
"notify": true,
"idempotency_key": "moderation-8841",
"error": null,
"end_balance": 307,
"created_at": "2026-09-23T14:13:05.120931Z"
}
}
GET /transfers — transfer history
Your account's API transfers, newest first.
| Parameter | Description |
|---|---|
limit | 1–100, default 20. |
cursor | next_cursor of the previous answer: the next page. |
status | pending | completed | refused |
idempotency_key | Find a transfer by your own key: handy when an answer was lost. |
telegram_id | Only transfers to this recipient. |
curl "https://shiba-bank.com/api/v1/transfers?limit=50&status=completed" -H "Authorization: Bearer $SHIBA_API_KEY"
{
"success": true,
"transfers": [
{
"id": "tr_5f0c2a9e41b7d3c8a6e1f024",
"status": "completed",
"telegram_id": 123456789,
"amount": 5,
"comment": "за модерацию никнейма",
"notify": true,
"idempotency_key": "moderation-8841",
"error": null,
"end_balance": 307,
"created_at": "2026-09-23T14:13:05.120931Z"
}
],
"next_cursor": "tr_5f0c2a9e41b7d3c8a6e1f024"
}
next_cursor is null on the last page.
GET /balance — balance
How many Stars your wallet has available right now.
curl https://shiba-bank.com/api/v1/balance -H "Authorization: Bearer $SHIBA_API_KEY"
{
"success": true,
"telegram_id": 555000111,
"balance": 312
}
GET /me — key, limits, balance
Everything about the key in one request: whose it is, the limits, today's use, allowed addresses.
{
"success": true,
"telegram_id": 555000111,
"username": "my_service_owner",
"balance": 312,
"key": {
"prefix": "sbk_Q2x9aZ1p…",
"created_at": "2026-09-23T10:00:00Z",
"last_used_at": "2026-09-23T14:12:44Z"
},
"limits": {
"per_transfer": 500,
"daily": 2000,
"used_today": 120,
"remaining_today": 1880,
"resets_at": "2026-09-24T00:00:00Z",
"allowed_ips": [
"203.0.113.7"
]
},
"api_version": "1.0"
}
GET /recipients/{telegram_id} — can they receive
A check before promising a reward: can_receive is true when the person has started @barboskich_stars_bot and their wallet is available. The API reveals no name or other data.
{
"success": true,
"telegram_id": 123456789,
"can_receive": true
}
If false, ask the person to open @barboskich_stars_bot and press Start: Stars are never sent to an account the bot does not know, or they would be lost.
GET /status — is the API up
No key needed. For monitoring.
{
"success": true,
"api_version": "1.0",
"time": "2026-09-23T14:13:05Z",
"docs": "/api/docs"
}
An OpenAPI 3.1 description for Postman, Swagger and code generators: /api/v1/openapi.json
Notification and comment
With notify: true the recipient gets a message from the bot in their own language:
From: @my_service_owner
💬 «for nickname moderation»
👛 Wallet · 312 Stars
- The sender is the key owner: their @username, or their name and id without one.
- Messages go through the bot's queue: usually within seconds, longer during mass payouts, but they arrive. The Stars are credited by the time the API answers; the message neither waits for them nor affects them.
- If the person blocked the bot there is no message, but the Stars are in their wallet anyway.
notify: falseis a silent credit: the Stars appear in the wallet without a message.
Comment rules
- Up to 255 characters after cleaning; emoji are fine.
- One line: line breaks and extra spaces become one space; invisible and control characters are removed.
- Not allowed: links, domains (
site.com),t.me/…, @mentions, hashtags: the answer iscomment_not_allowed. This keeps the bot's message from becoming an ad or a phishing lure. - The comment is stored with the transfer and returned in
transfer.comment.
Errors
{
"success": false,
"error": {
"code": "insufficient_balance",
"message": "Not enough Stars in the wallet.",
"balance": 3,
"required": 5
},
"request_id": "req_9b1e04c27d5a6f33"
}
| Code | HTTP | Meaning and what to do |
|---|---|---|
invalid_json | 400 | The body is not a UTF-8 JSON object, or a field appears twice. |
invalid_request | 400 | A field is missing, of the wrong type or out of range; field names it. |
unknown_field | 400 | A field the API does not know (often a typo such as ammount). |
invalid_comment | 400 | comment is not a string. |
comment_too_long | 400 | The comment is longer than 255 characters. |
comment_not_allowed | 400 | The comment contains a link, a domain, an @mention or a hashtag. |
idempotency_key_required | 400 | A transfer without an idempotency key. |
invalid_idempotency_key | 400 | The key has the wrong format, or the header and the body disagree. |
key_in_url | 400 | The API key was sent in the URL. Treat it as exposed and issue a new one. |
unauthorized | 401 | The key is missing, wrong or revoked. |
key_blocked | 403 | API access of this account is blocked by the administration. |
account_restricted | 403 | The key owner's account is restricted in the bot. |
ip_not_allowed | 403 | The request's address is not allowed. ip is the address the server saw. |
not_found | 404 | No such transfer (or it is not yours). |
recipient_not_found | 404 | The recipient has never started the bot, or their wallet is unavailable. |
idempotency_conflict | 409 | The key already belongs to a different transfer (recipient, amount, comment or notify differ). |
payload_too_large | 413 | The body is larger than 4096 bytes. |
unsupported_media_type | 415 | No Content-Type: application/json header. |
recipient_is_self | 422 | A transfer to yourself. |
per_transfer_limit_exceeded | 422 | The amount is above the per-transfer limit (limit). |
daily_limit_exceeded | 422 | The transfer would exceed the daily limit: limit, used_today, remaining. |
insufficient_balance | 422 | Not enough Stars: balance and required. Nothing moved; the same key may be repeated after a top-up. |
wallet_frozen | 422 | The sender's or the recipient's wallet is frozen or under review. Nothing moved. |
transfer_refused | 422 | The wallet refused the transfer. Nothing moved. |
account_too_new | 422 | The account cannot send Stars yet (hours_left). |
rate_limited | 429 | Too many requests. Wait Retry-After seconds. |
too_many_failed_attempts | 429 | Too many wrong-key requests from your address. Check the key and wait. |
internal_error | 500 | Our error. Contact support with the request_id. |
api_disabled | 503 | The API is switched off for now. |
busy | 503 | The transfer queue is busy. Repeat the same request after Retry-After. |
outcome_unknown | 503 | The outcome is not known yet. Repeat the same request with the same key: it never pays twice. |
Any 4xx answer means this request moved nothing. With 5xx the outcome may be unknown: repeat the same request with the same key.
Code examples
Python (requests)
import os
import time
import requests
API = "https://shiba-bank.com/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['SHIBA_API_KEY']}"}
class ShibaApiError(Exception):
def __init__(self, code: str, message: str, details: dict):
super().__init__(f"{code}: {message}")
self.code, self.details = code, details
def give_stars(telegram_id: int, amount: int, comment: str, key: str, notify: bool = True) -> int:
"""Credit Stars and return the balance left. Safe to call again with the same key."""
payload = {
"telegram_id": telegram_id,
"amount": amount,
"comment": comment,
"notify": notify,
"idempotency_key": key,
}
for attempt in range(6):
try:
response = requests.post(f"{API}/transfers", headers=HEADERS, json=payload, timeout=40)
except requests.RequestException:
time.sleep(2 ** attempt) # the network failed: the same key is safe to repeat
continue
if response.status_code in (429, 503) or response.status_code >= 500:
time.sleep(int(response.headers.get("Retry-After", 2 ** attempt)))
continue
data = response.json()
if data["success"]:
return data["end_balance"]
error = data["error"]
raise ShibaApiError(error["code"], error["message"], error)
raise RuntimeError("the API did not answer; repeat later with the same key")
balance = give_stars(123456789, 5, "за модерацию никнейма", key="moderation-8841")
print("left:", balance)
Node.js
// Node.js 18+ (global fetch)
const API = "https://shiba-bank.com/api/v1";
async function giveStars(telegramId, amount, comment, key, notify = true) {
const body = JSON.stringify({
telegram_id: telegramId, amount, comment, notify, idempotency_key: key,
});
for (let attempt = 0; attempt < 6; attempt++) {
let res;
try {
res = await fetch(`${API}/transfers`, {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.SHIBA_API_KEY}`,
"Content-Type": "application/json",
},
body,
});
} catch (e) {
await new Promise(r => setTimeout(r, 1000 * 2 ** attempt));
continue;
}
if (res.status === 429 || res.status >= 500) {
const wait = Number(res.headers.get("Retry-After") || 2 ** attempt);
await new Promise(r => setTimeout(r, 1000 * wait));
continue;
}
const data = await res.json();
if (data.success) return data.end_balance;
const err = new Error(`${data.error.code}: ${data.error.message}`);
err.details = data.error;
throw err;
}
throw new Error("the API did not answer; repeat later with the same key");
}
giveStars(123456789, 5, "за модерацию никнейма", "moderation-8841").then(console.log);
PHP
<?php
function give_stars(int $telegramId, int $amount, string $comment, string $key, bool $notify = true): int {
$ch = curl_init("https://shiba-bank.com/api/v1/transfers");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 40,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("SHIBA_API_KEY"),
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"telegram_id" => $telegramId,
"amount" => $amount,
"comment" => $comment,
"notify" => $notify,
"idempotency_key" => $key,
], JSON_UNESCAPED_UNICODE),
]);
$raw = curl_exec($ch);
curl_close($ch);
if ($raw === false) {
throw new RuntimeException("network error: repeat with the same key");
}
$data = json_decode($raw, true);
if (!$data["success"]) {
throw new RuntimeException($data["error"]["code"] . ": " . $data["error"]["message"]);
}
return $data["end_balance"];
}
Mass payout
There is no batch method: send transfers one by one, each with its own key (say payout-2026-09-23-<telegram_id>). If the script dies halfway, run it again from the start: completed transfers come back with replayed: true and are not charged twice.
for user_id, stars in winners:
give_stars(user_id, stars, "приз недели", key=f"weekly-2026-39-{user_id}")
FAQ
- Where do the Stars for transfers come from?
- Only from your wallet in @barboskich_stars_bot. Top it up in the bot: “👛 Wallet” → “Top up”.
- What can the recipient do with the Stars?
- Everything any wallet Stars allow: withdraw to their Telegram account, sell, give away in a chat, send as a cheque.
- Can a transfer be undone?
- No. A completed transfer is final, like
+send. Usedry_runto check first. - How do I get a person's Telegram id?
- Telegram sends it in every update to your bot:
message.from.id. The API does not send by @username: usernames change, ids do not. - How much does the API cost?
- Nothing. Transfers between wallets carry no fee.
- I got 503 outcome_unknown: did the Stars move?
- Repeat the same request with the same key: the final answer comes back, never a double charge. Or look at
GET /transfers?idempotency_key=…. - Need a webhook for incoming transfers, or to accept payment in Stars?
- Tell the bot's support what exactly you need: that decides what the next versions add.
Changelog
1.0 — 23.09.2026. Transfers, balance, limits, history, recipient check, OpenAPI.