iSpy Bot API

Complete REST API reference for IMEI checks, order management, and service catalog.

v1 — Stable
$ https://api.ispy.service/api/v1

🔒 Authentication

All API requests require an API key passed via the X-API-Key header. You can manage your API keys in the Telegram bot under My Profile → API.

Request Header
$ curl -H "X-API-Key: sk_live_your_api_key" \
     https://api.ispy.service/api/v1/balance
Security: Never share your API key publicly. Treat it like a password. If compromised, regenerate it immediately via the bot.

Getting an API Key

1. Open @ispyware_bot in Telegram
2. Go to My Profile → API
3. Request API access or create a new key
4. Copy the sk_live_... key for your integrations

Rate Limits

Default rate limit: 100 requests per minute per API key. Rate limit status is returned in response headers.

HeaderDescription
X-RateLimit-LimitMaximum requests per window
X-RateLimit-RemainingRemaining requests in current window
X-RateLimit-ResetUnix timestamp when window resets
Note: If you exceed the rate limit you will receive a 429 Too Many Requests response. Contact the admin to request a higher limit for your key.

Endpoints

GET /health Health check

Check API server availability. Does not require authentication.

Request
$ curl https://api.ispy.service/health
Response · 200 OK
{
  "status": "ok",
  "version": "1.0.0"
}
GET /api/v1/balance Account balance

Returns the current balance and currency for the authenticated account.

Request
$ curl -H "X-API-Key: sk_live_xxx" \
     https://api.ispy.service/api/v1/balance
Response · 200 OK
{
  "success": true,
  "data": {
    "balance": 25.50,
    "currency": "USD"
  }
}
GET /api/v1/services List services

Returns the catalog of available check services with pricing.

Request
$ curl -H "X-API-Key: sk_live_xxx" \
     https://api.ispy.service/api/v1/services
Response · 200 OK
{
  "success": true,
  "data": {
    "services": [
      {
        "id": 1,
        "name": "IMEI Check Basic",
        "category": "IMEI",
        "price": 1.50,
        "input_types": ["imei", "serial"],
        "accept_file": false,
        "description": "Basic IMEI info: model, blacklist, FMI",
        "delivery_time": "instant"
      },
      {
        "id": 14,
        "name": "FMI ON/OFF [S1]",
        "category": "iCloud",
        "price": 0.02,
        "input_types": ["imei", "serial"],
        "accept_file": false,
        "description": "Find My iPhone ON/OFF check",
        "delivery_time": "instant"
      },
      {
        "id": 42,
        "name": "Test: File Echo",
        "category": "Tools",
        "price": 0.01,
        "input_types": ["file"],
        "accept_file": true,
        "description": "Upload a file, get it back renamed",
        "delivery_time": "instant"
      }
    ],
    "total": 52
  }
}
POST /api/v1/orders Create order

Submit a new check order. The order cost is deducted from your balance automatically.

Request Body

ParameterTypeDescription
service_id required string ID of the service from /services (passed as string)
input_data required string IMEI (15 digits), serial number, or phone number depending on service
telegram_id required integer Your Telegram user ID (visible in bot under My Profile)
caption optional string Custom caption for file-based services. Displayed with the result file in Telegram. Max 1024 chars. Only used when the service has accept_file: true. See File-Based Services.
webhook_url optional string URL to receive result when order completes. Overrides the default webhook set on the API key. Must be http:// or https://, max 500 chars. See Webhooks.
Request
$ curl -X POST https://api.ispy.service/api/v1/orders \
     -H "X-API-Key: sk_live_xxx" \
     -H "Content-Type: application/json" \
     -d '{
  "service_id": "14",
  "input_data": "GW9F2X0R50",
  "telegram_id": 123456
}'
Response · 201 Created
{
  "success": true,
  "data": {
    "order_id": "7147",
    "status": "completed",
    "service_name": "FMI ON/OFF [S1]",
    "input_data": "GW9F2X0R50",
    "price": 0.02,
    "created_at": "2026-01-27T14:50:15Z"
  }
}
Note: Some services return results instantly (status: "completed"). Others require processing time and return status: "pending" — use GET /orders/{id} to poll, or configure a webhook.
Important: The input_data value is not validated at order creation time. The order will be accepted with status pending, and if the input is invalid, the worker will return status: "error" after processing. Check the final status via polling or webhook.
Error Response · 400 Validation Error
{
  "code": "VALIDATION_ERROR",
  "message": "Telegram ID is required",
  "localized_messages": {
    "en": "Telegram ID is required",
    "ru": "Ошибка валидации"
  }
}
GET /api/v1/orders/{id} Order status

Retrieve the status and result of a specific order.

Path Parameters

ParameterTypeDescription
id required string Order ID returned from POST /orders
Request
$ curl -H "X-API-Key: sk_live_xxx" \
     https://api.ispy.service/api/v1/orders/7147
Response · 200 OK (completed)
{
  "success": true,
  "data": {
    "order_id": "7147",
    "status": "completed",
    "service_name": "FMI ON/OFF [S1]",
    "input_data": "GW9F2X0R50",
    "output_data": {
      "find_my_watch": "ON"
    },
    "price": 0.02,
    "created_at": "2026-01-27T14:50:14Z",
    "updated_at": "2026-01-27T14:50:15Z"
  }
}
Response · 200 OK (pending)
{
  "success": true,
  "data": {
    "order_id": "7148",
    "status": "pending",
    "service_name": "GSX Full Report",
    "input_data": "353456789012345",
    "output_data": null,
    "price": 3.50,
    "created_at": "2026-01-27T15:12:00Z",
    "updated_at": "2026-01-27T15:12:00Z"
  }
}
GET /api/v1/orders List orders

List orders with filtering, sorting, and pagination.

Query Parameters

ParameterTypeDescription
status optionalstringpending | completed | error
service_id optionalintegerFilter by service ID
from_date optionalISO 8601Created from date, e.g. 2026-01-01T00:00:00Z
to_date optionalISO 8601Created to date
sort_by optionalstringcreated_at | updated_at | price
sort_dir optionalstringasc | desc (default: desc)
include_output optionalbooleanInclude output_data in response (default: true)
fields optionalstringComma-separated field names to return
page optionalintegerPage number (default: 1)
limit optionalintegerItems per page (default: 20, max: 100)
Request
$ curl -H "X-API-Key: sk_live_xxx" \
     "https://api.ispy.service/api/v1/orders?status=completed&limit=2&sort_by=created_at&sort_dir=desc"
Response · 200 OK
{
  "success": true,
  "data": {
    "orders": [
      {
        "order_id": "7147",
        "status": "completed",
        "service_name": "FMI ON/OFF [S1]",
        "input_data": "GW9F2X0R50",
        "output_data": { "find_my_watch": "ON" },
        "price": 0.02,
        "created_at": "2026-01-27T14:50:14Z"
      },
      {
        "order_id": "7140",
        "status": "completed",
        "service_name": "BlackList Check",
        "input_data": "353456789012345",
        "output_data": { "blacklisted": "Clean" },
        "price": 0.05,
        "created_at": "2026-01-27T12:30:00Z"
      }
    ],
    "total": 5184,
    "page": 1,
    "limit": 2
  }
}

📎 File-Based Services

Some services accept file input (documents, photos) instead of IMEI/serial numbers. These services are marked with accept_file: true in the GET /services response and include "file" in input_types.

How File Input Works

File-based services use Telegram file IDs as input. When a user sends a document or photo to the bot, Telegram assigns a unique file_id. This ID is passed in input_data using the following format:

FormatDescription
file:document:<file_id> Document file (PDF, XLSX, TXT, etc.)
file:photo:<file_id> Photo file (JPG, PNG)

Creating a File Order via API

Request · POST /api/v1/orders
$ curl -X POST https://api.ispy.service/api/v1/orders \
     -H "X-API-Key: sk_live_xxx" \
     -H "Content-Type: application/json" \
     -d '{
  "service_id": "42",
  "input_data": "file:document:BQACAgIAAxkBAAISkWnByZzcVaPB...",
  "telegram_id": 123456,
  "caption": "Process this invoice"
}'

Request Parameters for File Services

ParameterTypeDescription
input_data required string file:document:<file_id> or file:photo:<file_id>. The Telegram file ID of the uploaded file.
caption optional string Custom caption displayed with the result file in Telegram. Max 1024 characters. If omitted, a default localized message is used.

File Order Response

When a file-based service completes, the response includes a file_name field with the name of the result file.

GET /api/v1/orders/7200 · completed file order
{
  "success": true,
  "data": {
    "order_id": "7200",
    "status": "completed",
    "service_name": "Test: File Echo",
    "input_data": "file:document:BQACAgIAAxkB...",
    "output_data": "File received: file_0.pdf (413883 bytes), renamed to ok.txt",
    "file_name": "ok.txt",
    "price": 0.01,
    "created_at": "2026-03-24T10:00:00Z",
    "updated_at": "2026-03-24T10:00:02Z"
  }
}

Webhook Payload for File Orders

Webhook notifications for file orders include the file_name field:

Webhook POST Body · file order completed
{
  "order_id": "7200",
  "status": "completed",
  "input_data": "file:document:BQACAgIAAxkB...",
  "output_data": "File received: file_0.pdf, renamed to ok.txt",
  "file_name": "ok.txt",
  "price": 0.01,
  "completed_at": "2026-03-24T10:00:02Z"
}

Identifying File Services

Tip: Use the GET /services endpoint and filter by accept_file: true to find all file-accepting services. These services will also have "file" in the input_types array.
Important: File IDs are specific to the Telegram bot. A file_id obtained from one bot cannot be used with another. The file must have been previously sent to @ispyware_bot.

📦 Webhooks

Get notified via HTTP when an API order completes or fails — no polling required. Webhooks are sent only for orders created through the API (source = api); Telegram notifications are not sent for API orders.

Setting the Webhook URL

There are two ways to specify where the result is delivered. Per-request URL takes priority.

MethodPriorityDescription
Per-request Higher Pass webhook_url in the POST /api/v1/orders body. Applies only to that order.
Default (API key) Lower Set once in the Telegram bot: My Profile → API → select key → Webhook URL. Used when the request body has no webhook_url.
Tip: Use the default webhook on your API key for a "set and forget" setup. Override with webhook_url per request when you need results routed to a different endpoint.

Delivery Details

PropertyValue
HTTP methodPOST
Content-Typeapplication/json
Timeout30 seconds
SuccessAny 2xx status code
TriggerOrder completes (completed) or fails (error)
DeliveryAsynchronous — does not block order processing

Payload

Webhook POST Body · order completed
{
  "order_id": "7147",
  "status": "completed",
  "input_data": "GW9F2X0R50",
  "output_data": {
    "find_my_watch": "ON"
  },
  "price": 0.02,
  "completed_at": "2026-01-27T14:50:15Z"
}
Webhook POST Body · order failed
{
  "order_id": "7148",
  "status": "error",
  "input_data": "353456789012345",
  "output_data": null,
  "price": 3.50,
  "completed_at": "2026-01-27T15:14:22Z"
}

Payload Fields

FieldTypeDescription
order_idstringUnique order identifier — use for idempotency (duplicates possible on network retries)
statusstringcompleted or error
input_datastringIMEI / serial / phone submitted with the order
output_dataobject | nullCheck result. null when status is error
file_namestring | nullResult file name for file-based services (e.g. "ok.txt"). Omitted for non-file services. See File-Based Services
pricenumberAmount charged for this order (USD)
completed_atISO 8601Timestamp when the order was finalized

Example: Creating an Order with Webhook

Request · POST /api/v1/orders
$ curl -X POST https://api.ispy.service/api/v1/orders \
     -H "X-API-Key: sk_live_xxx" \
     -H "Content-Type: application/json" \
     -d '{
  "service_id": "14",
  "input_data": "GW9F2X0R50",
  "telegram_id": 123456,
  "webhook_url": "https://example.com/ispy-webhook"
}'
Response · 201 Created (order accepted, result will arrive at webhook)
{
  "success": true,
  "data": {
    "order_id": "7150",
    "status": "pending",
    "price": 0.02,
    "created_at": "2026-01-27T16:00:00Z"
  }
}

Limitations

Bulk orders: Webhooks are currently supported for single API orders only. Bulk orders do not trigger webhook delivery.

• Only http:// and https:// URLs are accepted (max 500 characters).
• HTTPS is strongly recommended for production endpoints.
• The HTTP client timeout is 30 seconds; responses with 2xx status are treated as success.
• Duplicate deliveries are possible on network failures — use order_id for idempotent processing.

Managing the Default Webhook in Bot

In @ispyware_bot:
My Profile → API Access → select key
Webhook URL — set or update the default URL
Reset webhook — clear the URL (disables webhook for this key)

Error Codes

All error responses follow a consistent JSON format:

Error Response Format
{
  "code": "ERROR_CODE",
  "message": "Human-readable description",
  "localized_messages": {
    "en": "English message",
    "ru": "Сообщение на русском"
  }
}
HTTP CodeError CodeDescription
400 INVALID_REQUEST_BODY Malformed JSON or missing required fields
400 VALIDATION_ERROR Request body fields failed validation (e.g. missing telegram_id)
401 unauthorized Missing or invalid API key
402 insufficient_balance Not enough balance to complete the order
403 forbidden API key does not have permission for this action
404 not_found Order or service not found
422 validation_error Input data doesn't match service requirements
429 rate_limited Too many requests, try again later
500 internal_error Server error, contact support
502 upstream_error External provider returned an error
503 service_unavailable API is temporarily down for maintenance

💻 Code Examples

Quick-start examples for common integrations.

Full IMEI Check Flow

# 1. Check balance
$ curl -s -H "X-API-Key: sk_live_xxx" \
     https://api.ispy.service/api/v1/balance

# 2. List available services
$ curl -s -H "X-API-Key: sk_live_xxx" \
     https://api.ispy.service/api/v1/services

# 3. Create an order
$ curl -s -X POST https://api.ispy.service/api/v1/orders \
     -H "X-API-Key: sk_live_xxx" \
     -H "Content-Type: application/json" \
     -d '{"service_id": "14", "telegram_id": 123456, "input_data": "GW9F2X0R50"}'

# 4. Check order status (if pending)
$ curl -s -H "X-API-Key: sk_live_xxx" \
     https://api.ispy.service/api/v1/orders/7147
import requests
import time

API_KEY = "sk_live_xxx"
BASE    = "https://api.ispy.service/api/v1"
headers = {"X-API-Key": API_KEY}

# Check balance
balance = requests.get(f"{BASE}/balance", headers=headers).json()
print(f"Balance: {balance['data']['balance']} {balance['data']['currency']}")

# Create order
order = requests.post(f"{BASE}/orders", headers=headers, json={
    "service_id": "14",
    "telegram_id": 123456,
    "input_data": "GW9F2X0R50"
}).json()

order_id = order["data"]["order_id"]
print(f"Order {order_id}: {order['data']['status']}")

# Poll if pending
while order["data"]["status"] == "pending":
    time.sleep(5)
    order = requests.get(
        f"{BASE}/orders/{order_id}", headers=headers
    ).json()

print("Result:", order["data"]["output_data"])
const API_KEY = "sk_live_xxx";
const BASE    = "https://api.ispy.service/api/v1";

const headers = {
  "X-API-Key": API_KEY,
  "Content-Type": "application/json"
};

// Check balance
const bal = await fetch(`${BASE}/balance`, { headers }).then(r => r.json());
console.log(`Balance: ${bal.data.balance} ${bal.data.currency}`);

// Create order
const order = await fetch(`${BASE}/orders`, {
  method: "POST",
  headers,
  body: JSON.stringify({
    service_id: "14",
    telegram_id: 123456,
    input_data: "GW9F2X0R50"
  })
}).then(r => r.json());

console.log(`Order ${order.data.order_id}: ${order.data.status}`);

// Poll if pending
let result = order;
while (result.data.status === "pending") {
  await new Promise(r => setTimeout(r, 5000));
  result = await fetch(
    `${BASE}/orders/${order.data.order_id}`, { headers }
  ).then(r => r.json());
}

console.log("Result:", result.data.output_data);
<?php
$apiKey = 'sk_live_xxx';
$base   = 'https://api.ispy.service/api/v1';

function apiGet($url, $key) {
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER     => ["X-API-Key: {$key}"],
    ]);
    $res = curl_exec($ch);
    curl_close($ch);
    return json_decode($res, true);
}

function apiPost($url, $key, $data) {
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => json_encode($data),
        CURLOPT_HTTPHEADER     => [
            "X-API-Key: {$key}",
            "Content-Type: application/json",
        ],
    ]);
    $res = curl_exec($ch);
    curl_close($ch);
    return json_decode($res, true);
}

// Check balance
$balance = apiGet("{$base}/balance", $apiKey);
echo "Balance: " . $balance['data']['balance'] . "\n";

// Create order
$order = apiPost("{$base}/orders", $apiKey, [
    'service_id' => '14',
    'telegram_id' => 123456,
    'input_data' => 'GW9F2X0R50',
]);

$orderId = $order['data']['order_id'];
echo "Order {$orderId}: " . $order['data']['status'] . "\n";

// Poll if pending
while ($order['data']['status'] === 'pending') {
    sleep(5);
    $order = apiGet("{$base}/orders/{$orderId}", $apiKey);
}

print_r($order['data']['output_data']);

Webhook Handler

from flask import Flask, request, jsonify

app = Flask(__name__)
processed = set()  # idempotency guard

@app.route("/ispy-webhook", methods=["POST"])
def handle_webhook():
    data = request.json
    order_id = data["order_id"]

    # Deduplicate (retries may send the same order_id)
    if order_id in processed:
        return jsonify({"ok": True}), 200
    processed.add(order_id)

    print(f"Order {order_id}: {data['status']}")
    print(f"  Input:  {data['input_data']}")
    print(f"  Price:  {data['price']}")

    if data["status"] == "completed":
        print(f"  Result: {data['output_data']}")
    else:
        print("  Order failed")

    return jsonify({"ok": True})

app.run(port=8080)
const express = require("express");
const app = express();
const processed = new Set();

app.use(express.json());

app.post("/ispy-webhook", (req, res) => {
  const { order_id, status, input_data, output_data, price } = req.body;

  // Deduplicate
  if (processed.has(order_id)) {
    return res.json({ ok: true });
  }
  processed.add(order_id);

  console.log(`Order ${order_id}: ${status}`);
  console.log(`  Input: ${input_data}, Price: ${price}`);

  if (status === "completed") {
    console.log("  Result:", output_data);
  } else {
    console.log("  Order failed");
  }

  res.json({ ok: true });
});

app.listen(8080);
<?php
// ispy-webhook.php
header('Content-Type: application/json');

$body = file_get_contents('php://input');
$data = json_decode($body, true);

if (!$data || empty($data['order_id'])) {
    http_response_code(400);
    echo json_encode(['error' => 'invalid payload']);
    exit;
}

$orderId = $data['order_id'];
$status  = $data['status'];

// TODO: check idempotency (e.g. DB lookup by order_id)

error_log("[webhook] Order {$orderId}: {$status}");

if ($status === 'completed') {
    $result = $data['output_data'];
    // Process result...
    error_log("[webhook] Result: " . json_encode($result));
}

echo json_encode(['ok' => true]);

Batch IMEI Check

Python
import requests, time

API_KEY = "sk_live_xxx"
BASE    = "https://api.ispy.service/api/v1"
headers = {"X-API-Key": API_KEY, "Content-Type": "application/json"}

imeis = ["353456789012345", "359876543210987", "GW9F2X0R50"]

# Submit all orders
order_ids = []
for imei in imeis:
    resp = requests.post(f"{BASE}/orders", headers=headers, json={
        "service_id": "14",
        "telegram_id": 123456,
        "input_data": imei
    }).json()
    if resp.get("success"):
        order_ids.append(resp["data"]["order_id"])
        print(f"Submitted {imei} -> order {resp['data']['order_id']}")

# Collect results
for oid in order_ids:
    for _ in range(20):
        r = requests.get(f"{BASE}/orders/{oid}", headers=headers).json()
        if r["data"]["status"] != "pending":
            print(f"Order {oid}: {r['data']['output_data']}")
            break
        time.sleep(3)