Start with a read-only API call, map one service, then build an order record that survives retries and restarts. That sequence gives you something more useful than a successful demo: a way to explain what happened to each customer purchase. This guide uses NotPanel’s current API contract and a small client example you can adapt in your own server application.
The key design decision is to keep three things separate: your customer’s purchase, the request used to place it, and the order returned by NotPanel. A connection can fail between those steps. If your application preserves all three identities, it can recover the result without treating every retry as another purchase.
1. Connect from your server and confirm authentication
New integrations use https://notpanel.com/api/v3. Send an HTTP POST with an application/x-www-form-urlencoded body (a JSON body also works) containing key and action. The /api/v1, /api/v2 and /api/v3 URLs behave identically. You can adopt optional features without changing familiar action names.
Create and manage your key on the API page. Keep it in your server’s private configuration. Do not embed it in a public page, place it in a URL, or write full authenticated request bodies to logs. The reason for avoiding URL credentials is practical: URLs can appear in access logs and other records, as the OWASP REST security guidance explains.
Begin with action=balance or action=services. These let you check your credentials and response handling without placing an order. Use placeholders in shared examples and supply the real key only in your private runtime configuration.
2. Keep the transport helper small and the result explicit
This JavaScript example handles one request. It returns HTTP context alongside the parsed result, including the error's error_code and retryable flag, and deliberately leaves retry decisions to the caller. A network failure or aborted request rejects the promise; catch that separately and preserve an uncertain add as unresolved. A non-JSON response also leaves the result unknown.
async function callNotPanel(apiKey, action, fields = {}, signal) {
// Run on your server. Never put apiKey in a browser bundle.
const response = await fetch("https://notpanel.com/api/v3", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({ ...fields, key: apiKey, action }),
signal,
});
const context = {
httpStatus: response.status,
correlationId: response.headers.get("X-Request-ID"),
retryAfter: response.headers.get("Retry-After"),
rateLimitReset: response.headers.get("X-RateLimit-Reset"),
};
let data;
try {
data = await response.json();
} catch {
return { kind: "unknown", ...context };
}
if (!response.ok || (data && typeof data.error === "string")) {
return {
kind: "error",
errorCode: data?.error_code ?? null,
retryable: data?.retryable === true,
data,
...context,
};
}
return { kind: "ok", data, ...context };
}Supply a cancellation signal and a time budget appropriate to the action in your application. Expiring that time budget stops your wait; it does not cancel an order already accepted by the panel. Also check the body and HTTP status: a completed fetch promise does not itself mean success. That distinction is part of the Fetch response model.
Validate the shape of each successful action before using it. For example, services returns a list, balance returns money fields, and add returns an order reference. This helper is the transport layer for your integration, not a complete checkout or automatic retry loop.
3. Map service IDs and requirements before offering a purchase
Store the numeric service ID as the identifier. A display name may change or be localized; it is not a safe lookup key. The services response supports language and exposes canonical_name and name_language so you can distinguish the displayed name from the canonical one.
Use action=services for the list. Each service now includes its fields, tags, drip_feed limits, step and availability. When your interface needs search or paginated discovery, action=catalog still works; both are covered in the services reference. Both approaches return service identifiers you can keep separate from display names.
Check the service type, minimum, maximum, and supported capabilities. Read fields for required inputs, such as comments, where the quantity must equal the number of comment lines, or Telegram reactions. A paused service still appears with availability set to paused and refuses new orders with SERVICE_PAUSED. A default quantity-based order is different from a package or custom-comments service. Do not send every service the same form. Use the catalog contract and place-order requirements to decide which fields your customer must supply.
Keep returned money strings as decimal values in your application’s money handling. Recheck availability and show the applicable price before purchase; a saved catalog entry is not a promise that a service or rate will remain unchanged. Preserve the charge returned for the accepted order in your own purchase record.
4. Save one request identity before placing the order
Generate request_id once per logical order, save it with the complete payload, and use it on every retry. The optional field is nonempty and at most 128 characters. idempotency_key is an accepted alias; request_id takes precedence when both are present.
Without either key, identical add requests from the same account have 60-second duplicate protection. Explicit keys let your client identify the order across a longer recovery schedule. Two intentionally separate purchases need different keys, even if their service, target, and quantity match. Keep that decision outside the retry loop and preserve it across application restarts.

Save the returned order reference against the original purchase. If add times out, replay the same key and parameters: the reply is the original order with replayed: true, or a single new order if the first attempt never arrived. You can also look the order up with action=order_details&request_id=… without resending it. REQUEST_ID_CONFLICT means the same key was used with different details. Do not switch to a new key to force an uncertain request through. The timeout recovery guide walks through those cases.
5. Track delivery and money as separate facts
For one order, send action=status&order=7001. For a batch, send action=status&orders=7001,7002, with at most 100 IDs. These are illustrative IDs: use the references returned for your account. Check every entry in a multi-status result because one missing order can produce an item-level error.
Use status_key for logic and status for the familiar display label. Do not decide that all work is complete merely because add returned an ID. Likewise, a Canceled or Partially completed status is not enough to reconstruct every movement in your own customer wallet, even though the undelivered part of a Partially completed order is refunded automatically.
| Field | Meaning for your integration |
|---|---|
| charge | Net charge after any refund. |
| refund | Refunded so far ("0.00" when nothing was refunded). For a Canceled or Partially completed order it is the refunded amount, returned with the order status. |
Do not calculate charge - refund: charge is already net of refunds, so subtracting refund again counts it twice. Use the status field definitions and order details for reconciliation. For cancellation, read can_cancel and cancel_available in status, and instant_cancel_until from the add reply: within the first 15 seconds a cancel is instant with a full refund; after that it is a request that the service may refuse. Then confirm the action’s result; a local “cancelled” label does not cancel the order on NotPanel.
6. Add events, keep reconciliation, and respect rate limits
A verified webhook receiver can update your customer view as selected events arrive. Each webhook carries one event; deduplicate on its id after signature verification. Keep status checks for recovery because events can be missed, duplicated, or delayed. The webhooks and polling guide explains where each belongs.
All of your API keys share the account’s request allowance. Creating another key does not add capacity. Read X-RateLimit-Remaining, obey Retry-After on limited requests, and treat X-RateLimit-Reset as relative seconds, not a Unix timestamp. X-RateLimit-DeniedBy, when supplied, helps identify the relevant limiter. Use the rate-limit reference instead of hard-coding one universal requests-per-minute value.
7. Prove the failure paths before launch
Test with mocks before making a real purchase. Simulate an accepted add whose response is lost, restart the client, and confirm the same request ID is reused. Simulate a key conflict, a rate-limit response, and an invalid JSON response. Each should produce an explicit recoverable or reviewable state in your application.
Then test a mixed-success status batch and the same webhook event twice. Confirm that one bad entry does not discard the rest of the batch and one duplicate event does not repeat a customer update. Keep a concise support trail with the action, time, HTTP status, error code, and X-Request-ID correlation ID where available. Redact the API key and private customer details. That correlation ID is a diagnostic reference; it is separate from your placement request_id.
For a final live check, use a small order you actually intend to purchase. Verify its returned reference, the order view, and the wallet entry. Passing a mocked retry test proves your client’s behaviour; the live check verifies the configured connection and the actual service you selected.
Frequently asked questions
Which NotPanel API URL should a new integration use?
Use https://notpanel.com/api/v3 with POST requests, form-encoded or JSON. The familiar key and action fields remain. The /api/v1, /api/v2 and /api/v3 paths behave identically; /api/v3 is the documented one.
Is request_id mandatory?
No. request_id and the idempotency_key alias are optional. Without either, identical add requests from one account receive 60-second duplicate protection. Persist an explicit key for each logical order when you control the client.
Does an HTTP 200 mean every item in a batch succeeded?
No. Multi-status results are keyed by order ID and individual entries can contain an error object, while the HTTP status stays 200. Validate each entry instead of treating the entire batch as a single successful order result.
Do I need webhooks before I can launch?
No. Batched polling is a valid starting point. Add a verified, durable webhook receiver when ready, and retain status checks to reconcile missing or delayed events.
Can I calculate a refund by subtracting refund from charge?
No. charge is already the net charge after any refund, so subtracting refund again counts the refund twice. charge plus refund gives the amount originally charged. For a Canceled or Partially completed order, refund is the refunded amount and comes with the order status.
Continue with the API reference for action details or the v3 compatibility guide when moving an existing reseller client to NotPanel.



