SHRU API documentation
The SHRU API is SHRU Cloud's own protocol for selling services to resellers and between SHRU Cloud platforms. One HTTPS endpoint, JSON in and out, and every request signed with HMAC-SHA256. This page has everything you need to connect: signing, every action, and sample orders for every kind of service.
On this page
Overview
- Endpoint
POST https://{platform domain}/api/shru- Body
- JSON, UTF-8, one
actionper request - Authentication
X-Key-Id,X-TimestampandX-Signature(HMAC-SHA256) headers- Actions
account_info,sync,submit_order,check_status- Replies
- JSON, always with
"success": trueorfalse - Version
- 1.0, sent back in the
shru-api-versionreply header
Every SHRU Cloud platform has the same endpoint on its own domain. You buy from a platform with credentials that platform gives your account, and you pay from your balance on that platform. Prices in the API are already the prices for your account.
https://shrucloud.com/docs/. It holds the whole protocol, a signing test vector and sample requests for every kind of order. The quick reference at the end sums it up in one block.Get your credentials
Each platform issues its own credentials. Ask the platform you buy from to turn on API access for your account. You then find everything in your account under My Account > API Access:
| Item | What it is |
|---|---|
| API URL | The platform's SHRU endpoint, for example https://platform.example.com/api/shru. |
| Key ID | Names your key. Sent with every request in X-Key-Id. Starts with shru_. |
| Secret | Signs your requests. It is never sent itself. Shown only once, when it is created or regenerated. |
| Allowed IP addresses | The addresses your requests may come from, up to 50. Requests from any other address are refused, and an empty list refuses every request. |
Make a request
Every call is an HTTPS POST to the API URL, with a JSON body and four headers.
| Header | Value |
|---|---|
Content-Type | application/json |
X-Key-Id | Your Key ID. |
X-Timestamp | The current Unix time in seconds. It must be within 5 minutes of the server's clock. |
X-Signature | The request's signature, see Sign every request. |
The body is a JSON object with action and that action's parameters. This is a complete request, signed with the test vector below:
POST /api/shru HTTP/1.1
Host: platform.example.com
Content-Type: application/json
X-Key-Id: shru_k7m2q9x4v8p3n6t1r5w0bd
X-Timestamp: 1767225600
X-Signature: e2bf80ed81ffaa94829122e0655d795c446609fadfb952c9025866d54f6b8fa5
{"action":"submit_order","service_id":1042,"input":"356789101234567"}Sign every request
The signature is an HMAC-SHA256 of four lines joined with a newline (\n), keyed with your Secret, written as lowercase hex.
signable = "POST" + "\n" + "/api/shru" + "\n" + timestamp + "\n" + body signature = lowercase_hex( HMAC_SHA256( key = Secret, message = signable ) )
- The body is the exact text you send. Encode the JSON once, sign that string, and send that same string. Encoding it again can change spacing or key order and break the signature.
- The path is always
/api/shruin the signed text. - The timestamp is the same value as the
X-Timestampheader. Unix time does not depend on time zones, so a refused timestamp means a wrong clock: keep your server in sync with NTP. - Sign every request again with a new timestamp. The same signed
submit_orderis accepted only once, so an identical order sent twice within one second is refused the second time.
Test vector
Check your code with these values. They must give exactly this signature.
- Secret
9f2d6c1e8b4a7d3f5e0c2b9a6d8f1e4c7b3a5d9e2f6c8b1a4d7e0f3c6b9a2d5e8f1c4b7a0d3e6f9c2b5a8d1e4f7c0b3a6d9e2f5c8b1a4d7e0f3c6b9a2d5e8f1c- X-Timestamp
1767225600- Body
{"action":"submit_order","service_id":1042,"input":"356789101234567"}- X-Signature
e2bf80ed81ffaa94829122e0655d795c446609fadfb952c9025866d54f6b8fa5
Code
A complete helper in four languages. Each one signs and sends any action and returns the decoded reply.
#!/usr/bin/env bash
KEY_ID="shru_your_key_id"
SECRET="your_secret"
ENDPOINT="https://platform.example.com/api/shru"
BODY='{"action":"account_info"}'
TS=$(date +%s)
SIG=$(printf 'POST\n/api/shru\n%s\n%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | sed 's/^.*= //')
curl -s -X POST "$ENDPOINT" \
-H "Content-Type: application/json" \
-H "X-Key-Id: $KEY_ID" \
-H "X-Timestamp: $TS" \
-H "X-Signature: $SIG" \
--data-raw "$BODY"<?php
const SHRU_KEY_ID = 'shru_your_key_id';
const SHRU_SECRET = 'your_secret';
const SHRU_ENDPOINT = 'https://platform.example.com/api/shru';
function shru(string $action, array $params = []): array
{
$body = json_encode(['action' => $action] + $params);
$ts = (string) time();
$sig = hash_hmac('sha256', "POST\n/api/shru\n{$ts}\n{$body}", SHRU_SECRET);
$ch = curl_init(SHRU_ENDPOINT);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $body,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 60,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'X-Key-Id: ' . SHRU_KEY_ID,
'X-Timestamp: ' . $ts,
'X-Signature: ' . $sig,
],
]);
$raw = curl_exec($ch);
$reply = json_decode((string) $raw, true);
return is_array($reply) ? $reply : ['success' => false, 'message' => 'No reply from the server'];
}
print_r(shru('account_info'));
print_r(shru('submit_order', ['service_id' => 1042, 'input' => '356789101234567']));// Node.js 18 or newer, saved as shru.mjs
import crypto from 'node:crypto';
const KEY_ID = 'shru_your_key_id';
const SECRET = 'your_secret';
const ENDPOINT = 'https://platform.example.com/api/shru';
async function shru(action, params = {}) {
const body = JSON.stringify({ action, ...params });
const ts = Math.floor(Date.now() / 1000).toString();
const sig = crypto
.createHmac('sha256', SECRET)
.update(`POST\n/api/shru\n${ts}\n${body}`)
.digest('hex');
const res = await fetch(ENDPOINT, {
method: 'POST',
redirect: 'error',
headers: {
'Content-Type': 'application/json',
'X-Key-Id': KEY_ID,
'X-Timestamp': ts,
'X-Signature': sig,
},
body,
});
return res.json();
}
console.log(await shru('account_info'));
console.log(await shru('submit_order', { service_id: 1042, input: '356789101234567' }));import hashlib, hmac, json, time, urllib.error, urllib.request
KEY_ID = "shru_your_key_id"
SECRET = "your_secret"
ENDPOINT = "https://platform.example.com/api/shru"
def shru(action, **params):
body = json.dumps({"action": action, **params}, separators=(",", ":")).encode()
ts = str(int(time.time()))
signable = b"POST\n/api/shru\n" + ts.encode() + b"\n" + body
sig = hmac.new(SECRET.encode(), signable, hashlib.sha256).hexdigest()
req = urllib.request.Request(ENDPOINT, data=body, method="POST", headers={
"Content-Type": "application/json",
"X-Key-Id": KEY_ID,
"X-Timestamp": ts,
"X-Signature": sig,
})
try:
with urllib.request.urlopen(req, timeout=60) as res:
return json.loads(res.read())
except urllib.error.HTTPError as err:
return json.loads(err.read())
print(shru("account_info"))
print(shru("submit_order", service_id=1042, input="356789101234567"))Replies and errors
Every reply is JSON. A successful one has "success": true and the action's own fields. A refused one has "success": false and a message that is safe to log or show to your user.
{
"success": true,
"order_id": 58210,
"status": "queued"
}{
"success": false,
"message": "Insufficient balance"
}| HTTP | Meaning |
|---|---|
| 200 | Done. Read the action's fields. |
| 400 | Understood but refused. The message says why (balance, service, input, quantity). |
| 401 | Authentication failed: a header is missing, the timestamp is too old, or the Key ID, Secret or signature is wrong. |
| 403 | Your IP address is not on the key's allowed list. |
| 405 | The request was not a POST. |
| 409 | The same signed request arrived twice. The same body in the same second gives the same signature, so sign again with a later timestamp. |
| 503 | The platform is under maintenance. The reply adds "maintenance": true and return_at, the planned end as Unix time (0 when not set). |
Messages you may see
| Message | What to do |
|---|---|
| Missing authentication headers | Send X-Key-Id, X-Timestamp and X-Signature. |
| Request timestamp expired or invalid | Send the current Unix time in seconds and fix your server clock. |
| Invalid credentials | Check the Key ID, and that API access is still on for your account. |
| Invalid signature | Sign the exact body you send, with the right Secret. Compare with the test vector. |
| Unauthorized access - this IP address is not permitted to use this API key | Add your server's IP address under Allowed IP addresses. |
| HTTPS is required | Use the https:// address. |
| Unknown action | Use one of the four actions on this page. |
| Invalid service ID / Service not found | Run sync again and use an id from it. |
| This service is not available through the API | Order it on the website instead. |
| Input is required | Send input: the service's input_required is true. |
| Input is too long for this service / Input is too long (255 characters at most) | Shorten input. |
| Quantity must be between 1 and 500 | Send a whole number within the service's quantity range. |
| Model is required / Model is too long | Fill that field in fields, up to 1000 characters. |
| Insufficient balance | Add funds to your account, then send the order again. |
| Out of stock | Nothing is available right now. Try again later. |
| This request was already received. Sign every request with a new timestamp. | You sent the same signed request again. Sign it again. |
| Invalid order ID / Order not found | Use an order_id returned to your own key's account. |
account_info
Your balance and account email. It changes nothing, so it is the best first call to test your setup.
{
"action": "account_info"
}{
"success": true,
"balance": 125.5,
"currency": "USD",
"email": "[email protected]"
}sync
Every service your account can order, with your own price, and your balance. Call it when you start and then about every 15 minutes to keep names, prices and fields current. A service that is not in the list cannot be ordered.
{
"action": "sync"
}{
"success": true,
"balance": 125.5,
"currency": "USD",
"services": [
{
"id": 1042,
"name": "iPhone Carrier + Find My Check",
"group": "Apple Checks",
"type": "fixed",
"price": 0.12,
"delivery": "Instant",
"input_label": "IMEI",
"input_required": true,
"quantity": null,
"bulk": true,
"fields": []
},
{
"id": 2210,
"name": "Samsung Network Unlock - All Models",
"group": "Samsung Unlock",
"type": "fixed",
"price": 4.5,
"delivery": "1-24 Hours",
"input_label": "IMEI",
"input_required": true,
"quantity": null,
"bulk": true,
"fields": [
{
"label": "Model",
"key": "Model",
"type": "input",
"required": true
},
{
"label": "Network",
"key": "Network",
"type": "input",
"required": false
}
]
},
{
"id": 3105,
"name": "Tool Credits",
"group": "Tool Credits",
"type": "quantity",
"price": 0.9,
"delivery": "Instant",
"input_label": "Username",
"input_required": true,
"quantity": {
"min": 1,
"max": 500
},
"bulk": false,
"fields": []
},
{
"id": 3120,
"name": "Tool Credits - Pack of 10",
"group": "Tool Credits",
"type": "quantity",
"price": 0.85,
"delivery": "Instant",
"input_label": "Username",
"input_required": true,
"quantity": {
"min": 10,
"max": 10
},
"bulk": false,
"fields": []
},
{
"id": 4001,
"name": "Gift Code $10",
"group": "Digital Codes",
"type": "fixed",
"price": 9.2,
"delivery": "Instant",
"input_label": null,
"input_required": false,
"quantity": null,
"bulk": false,
"fields": []
},
{
"id": 5150,
"name": "Unlock Tool Rental - 6 Hours",
"group": "Tool Rentals",
"type": "fixed",
"price": 2.5,
"delivery": "Instant",
"input_label": null,
"input_required": false,
"quantity": null,
"bulk": false,
"fields": []
}
]
}Service fields
| Field | Meaning |
|---|---|
id | The service_id to order with. |
name, group | Full name and group, for your own menus. |
type | fixed: one entry, one price. quantity: you choose how many. |
price | Your price in USD. For a quantity service it is the price of one unit. |
delivery | Usual delivery time, as text. |
input_label | What input must hold (IMEI, Serial Number, Username ...). null means the service takes no input. Never assume it is an IMEI. |
input_required | Whether input must be sent. |
quantity | {"min", "max"} for a quantity service, else null. |
bulk | The service takes many entries at once. Send one submit_order per entry. |
fields | Extra fields. Send each value in fields under its key, which can differ from its label. type is input or textarea. |
submit_order
Places an order. The price is taken from your balance when the order is accepted. Keep the order_id: you need it for check_status.
| Parameter | Type | Rules |
|---|---|---|
service_id | integer | Required. An id from sync. |
input | string | Required when input_required is true. Up to 255 characters. Leave it out when input_label is null. |
quantity | integer | Quantity services only. A whole number within quantity.min and quantity.max. Left out or 0 means the minimum. When min and max are the same, that amount is used. |
fields | object | Extra field values by key. Required fields must be filled. Each value up to 1000 characters. |
The reply holds order_id and status. Most orders start as queued. Instant delivery services answer completed straight away.
order_id and does not charge again. Any change in input, quantity or fields is a new order. This match needs an input or fields, and only covers orders still queued: an instant delivery order sent twice is two orders, so check account_info before you send one of those again.Sample orders
One sample for each kind of service, using the services from the sync sample above.
Fixed service with an IMEI
Service 1042 asks for an IMEI. Send all 15 digits.
{
"action": "submit_order",
"service_id": 1042,
"input": "356789101234567"
}{
"success": true,
"order_id": 58210,
"status": "queued"
}Fixed service with extra fields
Service 2210 needs Model and accepts Network. Each value goes under its field's key.
{
"action": "submit_order",
"service_id": 2210,
"input": "354829110987654",
"fields": {
"Model": "Galaxy S23 Ultra",
"Network": "T-Mobile USA"
}
}{
"success": true,
"order_id": 58211,
"status": "queued"
}Several entries at once (bulk)
Service 1042 has "bulk": true. There is no bulk parameter: send one request per IMEI. Each one is its own order with its own order_id and price.
{
"action": "submit_order",
"service_id": 1042,
"input": "356789101234567"
}{
"action": "submit_order",
"service_id": 1042,
"input": "356789101234575"
}Quantity service
Service 3105 sells credits from 1 to 500. Here 25 credits go to the account repairshop01: 25 x 0.90 = 22.50 USD.
{
"action": "submit_order",
"service_id": 3105,
"input": "repairshop01",
"quantity": 25
}{
"success": true,
"order_id": 58212,
"status": "queued"
}Quantity service sold in one amount
Service 3120 has min and max both 10, so every order is for 10 units (8.50 USD). quantity can be left out.
{
"action": "submit_order",
"service_id": 3120,
"input": "repairshop01"
}{
"success": true,
"order_id": 58213,
"status": "queued"
}Instant delivery, no input
Service 4001 delivers a code at once and takes no input (input_label is null). The order is completed in the same reply. Read the code with check_status.
{
"action": "submit_order",
"service_id": 4001
}{
"success": true,
"order_id": 58214,
"status": "completed"
}Tool rental
Service 5150 hands out a rental account for a set time. It works like the code above: completed at once, with the login details in check_status. When nothing is free, the order either waits as queued or is refused with Out of stock, depending on the platform's choice for that service.
{
"action": "submit_order",
"service_id": 5150
}{
"success": true,
"order_id": 58215,
"status": "completed"
}check_status
The current status of one of your orders and its result. reply is plain text with \n line breaks. It is empty until the order has a result, and on a rejected order it usually says why.
{
"action": "check_status",
"order_id": 58210
}{
"success": true,
"order_id": 58210,
"status": "processing",
"reply": ""
}{
"success": true,
"order_id": 58210,
"status": "completed",
"reply": "Model: iPhone 15 Pro\nCarrier: Unlocked\nFind My: OFF"
}{
"success": true,
"order_id": 58211,
"status": "rejected",
"reply": "Invalid IMEI"
}{
"success": true,
"order_id": 58214,
"status": "completed",
"reply": "GIFT-7KQ2-M9XA-4TPL"
}Order statuses
| Status | Meaning | Final |
|---|---|---|
queued | Accepted and paid, waiting to start. | No |
processing | Being worked on. | No |
completed | Done. The result is in reply. | Yes |
rejected | Could not be done. The full price is already back in your balance. | Yes |
refunded | Closed with the price returned to your balance. It can also follow completed when the platform refunds an order later. | Yes |
Check unfinished orders every 30 to 60 seconds and stop when the status is final. Many orders finish within a minute, others take as long as the service's delivery says.
Limits and good practice
- HTTPS only. Do not follow redirects: a signed request has exactly one right address.
- Keep your clock in sync. A timestamp more than 5 minutes off is refused.
- One action per request, signed with its own timestamp.
inputup to 255 characters, eachfieldsvalue up to 1000 characters.quantityis a whole number within the service's range.- Use the price from
sync. Prices are set per account and can change, and you are charged the price at the moment the order is accepted. - Run
syncabout every 15 minutes, and stop offering a service that is no longer listed. - Check your balance with
account_infobefore large batches. An order without enough balance is refused, not queued. - Store each
order_idbefore you show the order as placed.
SHRU to SHRU
If you run your own SHRU Cloud platform, you do not need any code to buy from another SHRU Cloud platform. Your platform speaks the SHRU API itself:
- Ask the other platform for API access for your account, and give them your platform server's IP address to allow.
- In your control center, open Suppliers, press Add Supplier and choose Shru Cloud.
- Enter the other platform's address as the API URL. Its domain is enough:
/api/shruis found by itself. - Enter the Key ID and the Secret, then save.
- Link their services to yours. Prices then update by themselves, and new orders go out automatically.
Selling to other platforms works the same way in reverse: give their account API access, and they add your platform as a supplier.
SDKs and starter kits
PHP SDK
A dependency-free client for PHP 7.2 and newer: sync, orders and status checks, with clear errors. MIT licensed.
Reseller Starter
A ready web app and command-line tool for buying from any SHRU Cloud platform, built on the PHP SDK.
Provider Starter
Run your own supplier panel that answers the SHRU API, with no database needed.
composer require shrucloud/php-sdk
<?php
use ShruCloud\Client;
use ShruCloud\ShruException;
$client = new Client('https://platform.example.com', 'shru_your_key_id', 'your_secret');
try {
$services = $client->syncServices();
$order = $client->placeOrder(1042, '356789101234567');
$status = $client->checkStatus($order['order_id']);
} catch (ShruException $e) {
echo $e->getMessage();
}Quick reference
The whole protocol in one block, for your notes or for an AI assistant.
SHRU API 1.0 - quick reference
Endpoint POST https://{platform domain}/api/shru (HTTPS only, JSON body, one action per request)
Headers Content-Type: application/json
X-Key-Id: {Key ID}
X-Timestamp: {Unix time in seconds, within 5 minutes of the server}
X-Signature: lowercase hex HMAC-SHA256(Secret, "POST\n/api/shru\n{timestamp}\n{exact body}")
Replies {"success": true, ...} or {"success": false, "message": "..."}
HTTP codes 200 ok | 400 refused (see message) | 401 authentication | 403 IP not allowed
405 not POST | 409 same signed request sent twice | 503 maintenance
account_info {"action":"account_info"}
-> balance, currency, email
sync {"action":"sync"}
-> balance, currency, services[]: id, name, group, type (fixed|quantity), price,
delivery, input_label (null = no input), input_required,
quantity {min,max} or null, bulk, fields[] {label, key, type, required}
submit_order {"action":"submit_order","service_id":1042,"input":"356789101234567"}
optional: "quantity": 25 (quantity services), "fields": {"<key>": "<value>"}
-> order_id, status (queued, or completed for instant delivery)
check_status {"action":"check_status","order_id":58210}
-> order_id, status (queued|processing|completed|rejected|refunded), reply (plain text)
Rules input up to 255 characters, each field value up to 1000 characters
quantity: whole number within min-max; left out = min; min = max means that amount
bulk: one submit_order per entry
the same order (with input or fields) sent again while the first is still queued returns the first
order_id with no second charge; instant delivery orders are not matched
poll unfinished orders every 30-60 seconds; sync about every 15 minutes
a service missing from sync cannot be ordered ("Service not found")
Test vector Secret 9f2d6c1e8b4a7d3f5e0c2b9a6d8f1e4c7b3a5d9e2f6c8b1a4d7e0f3c6b9a2d5e8f1c4b7a0d3e6f9c2b5a8d1e4f7c0b3a6d9e2f5c8b1a4d7e0f3c6b9a2d5e8f1c
Timestamp 1767225600
Body {"action":"submit_order","service_id":1042,"input":"356789101234567"}
Signature e2bf80ed81ffaa94829122e0655d795c446609fadfb952c9025866d54f6b8fa5