notpanel
ServicesPricingFAQGiveaway
notpanel

The fastest and most affordable SMM panel. Trusted by 1M+ users worldwide.

Product

  • Services
  • Pricing
  • Why NotPanel
  • About
  • Developers
  • Blog
  • FAQ

Legal

  • Terms of Service
  • Privacy Policy
  • Refund Policy

Connect

  • Contact Us
  • support@notpanel.com

© © 2026 NotPanel. All rights reserved.

All posts
Engineering April 18, 2026· 14 min read

SMM panel API integration: complete guide with code examples

How to integrate an SMM panel API into your application. v2 spec walkthrough, authentication, idempotent order placement, status polling vs webhooks, and the bugs that cost real money.


Most SMM panels expose a REST API that follows the same convention. One endpoint, form-encoded body, a key field for auth, an action field that selects the operation. The convention is called "v2" across the industry. Once you've integrated against one v2 panel, every other one is a base-URL swap away.

This guide is the practical reality of integrating. The parts of the spec that matter, the parts that vary between panels, and how to build a robust client that doesn't lose you money. Code samples target JavaScript and Python; the patterns translate.

Why integrate against an API at all?

For occasional manual orders, the dashboard is fine. The API matters the moment you're either:

  • Running a downstream business that needs to forward orders to your upstream provider in real time. (You're a reseller.)
  • Automating order placement based on triggers. New client signup, campaign launch, scheduled drops.
  • Building a dashboard, analytics layer, or report that aggregates across multiple sources.
  • Bulk-placing orders during high-volume promotional periods where manual entry is impractical.

The v2 contract in two paragraphs

Every request is an HTTP POST to a single endpoint (typically https://<panel>/api/v2) with an application/x-www-form-urlencoded body. The body always contains key (your API key) and action (the operation name). Other fields depend on the action.

Responses are JSON. Successes contain action-specific fields. Errors are { "error": "..." } with a 4xx (client) or 5xx (server) status. Idempotency for order placement is achieved via a caller-supplied request_id — repeated calls with the same value return the same response, never a duplicate order.

Your code+ request_idPOST /api/v2form-urlencodedPanelValidate · debit · enqueueResponseJSON · order IDRetry on 5xx with the SAME request_id
Request flow. The dashed retry path is what you must handle in client code.

The minimum viable client

Here's a JavaScript client with just enough abstraction to be useful. Auth, body encoding, JSON parsing, typed call() method.

const PANEL_BASE_URL = "https://notpanel.com/api/v2";

class SmmClient {
  constructor(apiKey, baseUrl = PANEL_BASE_URL) {
    this.apiKey = apiKey;
    this.baseUrl = baseUrl;
  }

  async call(action, params = {}) {
    const body = new URLSearchParams({
      key: this.apiKey,
      action,
      ...params,
    });

    const res = await fetch(this.baseUrl, {
      method: "POST",
      headers: { "Content-Type": "application/x-www-form-urlencoded" },
      body,
    });

    const data = await res.json();
    if (!res.ok || data.error) {
      throw new SmmApiError(data.error ?? "Unknown error", res.status);
    }
    return data;
  }

  services() { return this.call("services"); }
  balance() { return this.call("balance"); }
  status(orderId) { return this.call("status", { order: orderId }); }
  cancel(orderId) { return this.call("cancel", { order: orderId }); }
  refill(orderId) { return this.call("refill", { order: orderId }); }

  add(params) {
    if (!params.request_id) {
      throw new Error("request_id is required for idempotency");
    }
    return this.call("add", params);
  }
}

class SmmApiError extends Error {
  constructor(message, status) {
    super(message);
    this.name = "SmmApiError";
    this.status = status;
  }
}

Listing services

The services action returns the full catalog. Each entry has a stable numeric ID, name, per-1,000 rate, min/max bounds, and capability flags. Cache the result — most panels return the same data for many minutes, and the catalog rarely changes within a session.

const client = new SmmClient(process.env.SMM_API_KEY);

const services = await client.services();
console.log(`Loaded ${services.length} services`);

const cheapestIgFollowers = services
  .filter(s => s.name.toLowerCase().includes("instagram followers"))
  .sort((a, b) => parseFloat(a.rate) - parseFloat(b.rate))[0];

console.log(`Cheapest IG followers: $${cheapestIgFollowers.rate} per 1K`);

Placing orders with idempotency

Order placement is the only mutating action where idempotency is non-negotiable. Generate a UUID at order-creation time. Pass it as request_id on every retry of the same logical order. Don't, and network glitches plus timeout retries will create duplicate orders. You'll be charged for both.

Don't generate request_id server-side

The whole point of caller-supplied idempotency is that the SAME caller, retrying the SAME request, sends the SAME key. If you generate the key inside your retry loop, every retry has a different key — you've defeated the mechanism. Generate once, outside the retry loop.

import { randomUUID } from "node:crypto";

async function placeOrder(client, serviceId, link, quantity) {
  const requestId = randomUUID(); // ← outside the retry loop

  for (let attempt = 1; attempt <= 3; attempt++) {
    try {
      const result = await client.add({
        service: serviceId,
        link,
        quantity: String(quantity),
        request_id: requestId,
      });
      return result.order;
    } catch (err) {
      if (err.status >= 500 && attempt < 3) {
        await new Promise(r => setTimeout(r, 1000 * attempt));
        continue;
      }
      throw err;
    }
  }
}

Status — polling vs webhooks

After placing an order, you need to know when it completes. Two approaches.

Polling. Call action=status periodically with the order ID. Simple. Expensive in rate-limit terms. Introduces lag. The batch form (orders= with comma-separated IDs) is much cheaper than per-order polling.

async function pollUntilDone(client, orderId, maxWaitSec = 3600) {
  const start = Date.now();
  while (Date.now() - start < maxWaitSec * 1000) {
    const result = await client.status(orderId);
    if (["completed", "partial", "cancelled", "refunded"].includes(result.status)) {
      return result;
    }
    await new Promise(r => setTimeout(r, 30_000));
  }
  throw new Error("Order did not complete within max wait time");
}

Webhooks. Register an endpoint with the panel. Receive a POST whenever an order changes state. Order of magnitude less work, real-time updates, no rate-limit pressure. Strongly preferred for any non-trivial integration.

await client.call("webhook.add", {
  url: "https://your-server.example.com/notpanel-webhook",
  events: "order.completed,order.partial,order.failed,balance.low",
});
Reader poll

How do reseller apps integrated against NotPanel actually consume order updates?

Webhooks (signed, idempotent)64%
Webhooks + polling fallback22%
Polling only (multi-status batch)11%
Polling only (per-order)3%
Aggregate of integrations using the NotPanel API for 30+ days, sample of 218.

Verifying webhook signatures

Public webhook URLs are by definition reachable by attackers. Without signature verification, anyone who guesses your URL can fabricate completion notifications, fake refunds, trigger your downstream logic. Always verify.

import { createHmac, timingSafeEqual } from "node:crypto";

function verifyWebhook(rawBody, signatureHeader, timestampHeader, secret) {
  // 1. Reject deliveries older than 5 minutes (replay protection).
  const ageSec = Math.abs(Date.now() / 1000 - parseInt(timestampHeader, 10));
  if (ageSec > 300) return false;

  // 2. Compute expected HMAC of the raw body.
  const expected = createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");

  // 3. Constant-time compare against the provided signature.
  const a = Buffer.from(signatureHeader, "hex");
  const b = Buffer.from(expected, "hex");
  if (a.length !== b.length) return false;
  return timingSafeEqual(a, b);
}

Error handling that doesn't lose money

The two failure modes that cost real money:

  1. Duplicate orders from missing idempotency. You place an order, response times out, you retry, the panel accepts both. You're charged twice. Only one is the order you wanted. Always pass request_id.
  2. Lost orders from over-aggressive error handling. Panel returns 5xx, your code logs and moves on, the order actually went through but you don't know. Now you have a customer charged for a service you have no record of.
Treat 5xx as "unknown outcome", not "failure". Retry with the same key. Then check status. Never assume failure from a single error response.

Rate-limit etiquette

Most panels run layered limits — per-IP, per-key, per-tier. Polling status on hundreds of in-flight orders is the most common cause of unnecessary 429s. Practical mitigations:

  • Switch to webhooks for status wherever possible.
  • Use the multi-status form (orders=1,2,3,...) to batch up to 100 IDs in a single request.
  • Honour X-RateLimit-Reset on a 429. Sleep until that timestamp. Don't retry sooner.
  • Check X-RateLimit-DeniedBy to know which limit fired — distributing across more keys helps for per-key denials but nothing for per-IP.
300
req/min
API v2 default cap
100
Batch size
Multi-status max IDs
5min
Replay window
Webhook timestamp
3
Retries
Recommended on 5xx

What I'd build differently the second time

After integrating against several panels, the patterns that pay off:

  • A single SmmClient abstraction per panel, with a consistent error class and idempotency baked in. Don't sprinkle fetch() across your codebase.
  • A persistent record of every order placed, with its request_id, in your own database. When orders go missing or get rejected, you have the source of truth to reconcile against the panel's view.
  • A retry layer at the SmmClient boundary with exponential backoff and idempotency awareness. Not at every call site.
  • Webhooks first, polling as fallback. Polling is acceptable while you haven't integrated webhooks yet — but it should be the temporary state, not the steady state.

For NotPanel's specific implementation of the v2 contract — exact parameter names, response shapes, error codes — see the developer documentation. Patterns above apply across panels; implementation details vary in small ways, and the docs are the authoritative source for our specific case.

Continue reading

Reseller business

Bulk SMM orders: how agencies process 100,000+ orders per day

Tactics

Choosing the right SMM panel: 12 signals of a real source vs a reseller

Engineering

Webhook signing for SMM panels: HMAC-SHA256 in production