SHRU CloudAPI documentation

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 action per request
Authentication
X-Key-Id, X-Timestamp and X-Signature (HMAC-SHA256) headers
Replies
JSON, always with "success": true or false
Version
1.0, sent back in the shru-api-version reply 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.

Building with an AI assistant? Give it the address of this page, 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:

ItemWhat it is
API URLThe platform's SHRU endpoint, for example https://platform.example.com/api/shru.
Key IDNames your key. Sent with every request in X-Key-Id. Starts with shru_.
SecretSigns your requests. It is never sent itself. Shown only once, when it is created or regenerated.
Allowed IP addressesThe addresses your requests may come from, up to 50. Requests from any other address are refused, and an empty list refuses every request.
Keep the Secret on your server. Never put it in a browser, a mobile app or a public repository. If it leaks, press Regenerate: the old Secret stops working at once.

Make a request

Every call is an HTTPS POST to the API URL, with a JSON body and four headers.

HeaderValue
Content-Typeapplication/json
X-Key-IdYour Key ID.
X-TimestampThe current Unix time in seconds. It must be within 5 minutes of the server's clock.
X-SignatureThe 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:

HTTP request
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.

Signing
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/shru in the signed text.
  • The timestamp is the same value as the X-Timestamp header. 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_order is 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
{
  "success": true,
  "order_id": 58210,
  "status": "queued"
}
Refused
{
  "success": false,
  "message": "Insufficient balance"
}
HTTPMeaning
200Done. Read the action's fields.
400Understood but refused. The message says why (balance, service, input, quantity).
401Authentication failed: a header is missing, the timestamp is too old, or the Key ID, Secret or signature is wrong.
403Your IP address is not on the key's allowed list.
405The request was not a POST.
409The same signed request arrived twice. The same body in the same second gives the same signature, so sign again with a later timestamp.
503The 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

MessageWhat to do
Missing authentication headersSend X-Key-Id, X-Timestamp and X-Signature.
Request timestamp expired or invalidSend the current Unix time in seconds and fix your server clock.
Invalid credentialsCheck the Key ID, and that API access is still on for your account.
Invalid signatureSign 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 keyAdd your server's IP address under Allowed IP addresses.
HTTPS is requiredUse the https:// address.
Unknown actionUse one of the four actions on this page.
Invalid service ID / Service not foundRun sync again and use an id from it.
This service is not available through the APIOrder it on the website instead.
Input is requiredSend 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 500Send a whole number within the service's quantity range.
Model is required / Model is too longFill that field in fields, up to 1000 characters.
Insufficient balanceAdd funds to your account, then send the order again.
Out of stockNothing 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 foundUse 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.

Request body
{
  "action": "account_info"
}
Reply
{
  "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.

Request body
{
  "action": "sync"
}
Reply
{
  "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

FieldMeaning
idThe service_id to order with.
name, groupFull name and group, for your own menus.
typefixed: one entry, one price. quantity: you choose how many.
priceYour price in USD. For a quantity service it is the price of one unit.
deliveryUsual delivery time, as text.
input_labelWhat input must hold (IMEI, Serial Number, Username ...). null means the service takes no input. Never assume it is an IMEI.
input_requiredWhether input must be sent.
quantity{"min", "max"} for a quantity service, else null.
bulkThe service takes many entries at once. Send one submit_order per entry.
fieldsExtra 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.

ParameterTypeRules
service_idintegerRequired. An id from sync.
inputstringRequired when input_required is true. Up to 255 characters. Leave it out when input_label is null.
quantityintegerQuantity 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.
fieldsobjectExtra 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.

Safe to retry. If a request times out, send the same order again with a new timestamp, at least one second later. While the first copy is still queued, the platform answers with the same 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.

Request body
{
  "action": "submit_order",
  "service_id": 1042,
  "input": "356789101234567"
}
Reply
{
  "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.

Request body
{
  "action": "submit_order",
  "service_id": 2210,
  "input": "354829110987654",
  "fields": {
    "Model": "Galaxy S23 Ultra",
    "Network": "T-Mobile USA"
  }
}
Reply
{
  "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.

Request 1
{
  "action": "submit_order",
  "service_id": 1042,
  "input": "356789101234567"
}
Request 2
{
  "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.

Request body
{
  "action": "submit_order",
  "service_id": 3105,
  "input": "repairshop01",
  "quantity": 25
}
Reply
{
  "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.

Request body
{
  "action": "submit_order",
  "service_id": 3120,
  "input": "repairshop01"
}
Reply
{
  "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.

Request body
{
  "action": "submit_order",
  "service_id": 4001
}
Reply
{
  "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.

Request body
{
  "action": "submit_order",
  "service_id": 5150
}
Reply
{
  "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.

Request body
{
  "action": "check_status",
  "order_id": 58210
}
Still working
{
  "success": true,
  "order_id": 58210,
  "status": "processing",
  "reply": ""
}
Completed
{
  "success": true,
  "order_id": 58210,
  "status": "completed",
  "reply": "Model: iPhone 15 Pro\nCarrier: Unlocked\nFind My: OFF"
}
Rejected
{
  "success": true,
  "order_id": 58211,
  "status": "rejected",
  "reply": "Invalid IMEI"
}
Instant code
{
  "success": true,
  "order_id": 58214,
  "status": "completed",
  "reply": "GIFT-7KQ2-M9XA-4TPL"
}

Order statuses

StatusMeaningFinal
queuedAccepted and paid, waiting to start.No
processingBeing worked on.No
completedDone. The result is in reply.Yes
rejectedCould not be done. The full price is already back in your balance.Yes
refundedClosed 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.
  • input up to 255 characters, each fields value up to 1000 characters.
  • quantity is 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 sync about every 15 minutes, and stop offering a service that is no longer listed.
  • Check your balance with account_info before large batches. An order without enough balance is refused, not queued.
  • Store each order_id before 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:

  1. Ask the other platform for API access for your account, and give them your platform server's IP address to allow.
  2. In your control center, open Suppliers, press Add Supplier and choose Shru Cloud.
  3. Enter the other platform's address as the API URL. Its domain is enough: /api/shru is found by itself.
  4. Enter the Key ID and the Secret, then save.
  5. 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.

PHP SDK
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
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