Tryb testowy
Bez salda i bez dostawcy, z pełną walidacją.
Bazowy URL: https://sablex.pl/api/v1. Cena, rabat 5%, bezpieczeństwo marży i saldo są liczone wyłącznie na serwerze.
Bez salda i bez dostawcy, z pełną walidacją.
IPv4, IPv6 i CIDR osobno dla każdego klucza.
Kolejka, osiem prób i odporność na duplikaty.
Prefiks klucza określa środowisko na stałe. Środowiska mają osobne zamówienia, odczyt statusu i idempotency.
sblx_test_Brak pobrania salda
Brak wysyłki do dostawcy
Numer TST-*
Pełna walidacja i cena
sblx_live_Atomowe pobranie z salda
Automatyczna realizacja
Numer SAB-*
Prawdziwe statusy dostawcy
Najbezpieczniejszy sposób autoryzacji:
Authorization: Bearer sblx_test_xxxxxxxxxxxxxxxxxKażdy klucz ma scopes services:read, orders:read i orders:write, limit 60 żądań/min oraz opcjonalnie do 32 adresów lub CIDR.
203.0.113.10/32
198.51.100.0/24
2001:db8:abcd::/48 403 ip_not_allowed przed wykonaniem operacji.Aktywny katalog można filtrować po platformie:
curl 'https://sablex.pl/api/v1/services?platform=instagram' \
-H 'Authorization: Bearer sblx_test_xxx'Każdy rekord zawiera typ linku, limity, cenę retail, cenę reseller, wymagania komentarzy, czas usługi i liczbę dni refill.
Nagłówek Idempotency-Key jest obowiązkowy i ma 8–128 znaków.
POST /api/v1/orders
Authorization: Bearer sblx_test_xxx
Content-Type: application/json
Idempotency-Key: order-20260723-000001
{
"serviceId": "instagram-followers-world-846",
"link": "https://www.instagram.com/sablexpl/",
"quantity": 100,
"externalOrderId": "shop-order-4519"
}Identyczne ponowienie zwraca ten sam order i nagłówek Idempotency-Replayed: true. Ten sam klucz z innymi danymi zwraca 409 idempotency_conflict.
GET /api/v1/orders/TST-2026-100001
Authorization: Bearer sblx_test_xxx<?php
$apiKey = getenv('SABLEX_API_KEY');
$idempotencyKey = 'order-' . bin2hex(random_bytes(12));
$payload = [
'serviceId' => 'instagram-followers-world-846',
'link' => 'https://www.instagram.com/sablexpl/',
'quantity' => 100,
'externalOrderId' => 'shop-order-4519',
];
$curl = curl_init('https://sablex.pl/api/v1/orders');
curl_setopt_array($curl, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 20,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
'Idempotency-Key: ' . $idempotencyKey,
],
CURLOPT_POSTFIELDS => json_encode($payload, JSON_THROW_ON_ERROR),
]);
$body = curl_exec($curl);
if ($body === false) throw new RuntimeException(curl_error($curl));
$status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
curl_close($curl);
$result = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
if ($status < 200 || $status >= 300) {
throw new RuntimeException($result['error']['code'] . ': ' . $result['error']['message']);
}
print_r($result['data']);import os
import secrets
import requests
response = requests.post(
"https://sablex.pl/api/v1/orders",
headers={
"Authorization": f"Bearer {os.environ['SABLEX_API_KEY']}",
"Content-Type": "application/json",
"Idempotency-Key": f"order-{secrets.token_hex(12)}",
},
json={
"serviceId": "instagram-followers-world-846",
"link": "https://www.instagram.com/sablexpl/",
"quantity": 100,
"externalOrderId": "shop-order-4519",
},
timeout=20,
)
payload = response.json()
if not response.ok:
error = payload.get("error", {})
raise RuntimeError(f"{error.get('code')}: {error.get('message')}")
print(payload["data"])import { randomUUID } from "node:crypto";
const response = await fetch("https://sablex.pl/api/v1/orders", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SABLEX_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": `order-${randomUUID()}`,
},
body: JSON.stringify({
serviceId: "instagram-followers-world-846",
link: "https://www.instagram.com/sablexpl/",
quantity: 100,
externalOrderId: "shop-order-4519",
}),
signal: AbortSignal.timeout(20_000),
});
const payload = await response.json();
if (!response.ok) {
throw new Error(`${payload.error?.code}: ${payload.error?.message}`);
}
console.log(payload.data);{
"success": false,
"error": {
"code": "insufficient_balance",
"message": "Niewystarczające saldo Sablex.",
"details": {
"available_balance": 10,
"required_amount": 12.5,
"missing_amount": 2.5,
"currency": "PLN"
}
}
}invalid_api_keyinsufficient_balanceip_not_allowed / insufficient_scopeidempotency_conflict / external_order_existsidempotency_in_progressrate_limit_exceededKonfiguracja znajduje się w panelu API. Obsługiwane zdarzenia:
order.createdorder.processingorder.completedorder.failedorder.refundedSablex-Event: order.completed
Sablex-Event-Id: 7d9b...
Sablex-Delivery-Id: 23a1...
Sablex-Timestamp: 1784779000
Sablex-Signature: v1=5c7d...
Sablex-Livemode: true{
"id": "7d9b9c72-1111-4444-8888-123456789abc",
"type": "order.completed",
"created_at": "2026-07-23T05:10:00.000Z",
"livemode": true,
"data": {
"object": {
"object": "order",
"order_number": "SAB-2026-100050",
"external_order_id": "shop-order-4519",
"status": "completed",
"payment_status": "paid",
"test_mode": false
}
}
}Oblicz HMAC dla dokładnego ciągu timestamp.raw_body i porównaj z wartością po v1= w stałym czasie. Nie serializuj JSON ponownie.
hex_hmac_sha256(webhook_secret, Sablex-Timestamp + "." + RAW_BODY)<?php
$secret = getenv('SABLEX_WEBHOOK_SECRET');
$rawBody = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_SABLEX_TIMESTAMP'] ?? '';
$header = $_SERVER['HTTP_SABLEX_SIGNATURE'] ?? '';
$provided = str_starts_with($header, 'v1=') ? substr($header, 3) : '';
if (!ctype_digit($timestamp) || abs(time() - (int) $timestamp) > 300) {
http_response_code(400); exit('stale timestamp');
}
$expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
if (!$provided || !hash_equals($expected, $provided)) {
http_response_code(401); exit('invalid signature');
}
$event = json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);
http_response_code(200); echo 'ok';import hashlib
import hmac
import os
import time
from flask import Flask, request, abort
app = Flask(__name__)
secret = os.environ["SABLEX_WEBHOOK_SECRET"].encode()
@app.post("/webhooks/sablex")
def sablex_webhook():
raw_body = request.get_data(cache=False)
timestamp = request.headers.get("Sablex-Timestamp", "")
signature = request.headers.get("Sablex-Signature", "")
if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > 300:
abort(400)
provided = signature[3:] if signature.startswith("v1=") else ""
expected = hmac.new(
secret, timestamp.encode() + b"." + raw_body, hashlib.sha256
).hexdigest()
if not provided or not hmac.compare_digest(expected, provided):
abort(401)
event = request.get_json(force=True)
return "ok", 200import crypto from "node:crypto";
import express from "express";
const app = express();
app.post(
"/webhooks/sablex",
express.raw({ type: "application/json" }),
(request, response) => {
const timestamp = request.header("Sablex-Timestamp") || "";
const signature = request.header("Sablex-Signature") || "";
const provided = signature.startsWith("v1=") ? signature.slice(3) : "";
if (!/^\d+$/.test(timestamp) || Math.abs(Date.now()/1000-Number(timestamp)) > 300) {
return response.status(400).send("stale timestamp");
}
const expected = crypto.createHmac("sha256", process.env.SABLEX_WEBHOOK_SECRET)
.update(`${timestamp}.`).update(request.body).digest("hex");
const left = Buffer.from(expected, "hex");
const right = Buffer.from(provided, "hex");
if (left.length !== right.length || !crypto.timingSafeEqual(left, right)) {
return response.status(401).send("invalid signature");
}
const event = JSON.parse(request.body.toString("utf8"));
return response.status(200).send("ok");
}
);Za sukces uznawane jest tylko HTTP 2xx. Timeout próby wynosi 10 sekund, a redirecty nie są automatycznie śledzone.
1 min → 5 min → 15 min → 1 h → 6 h → 24 h → 48 h → 7 dniPo ósmej nieudanej próbie dostawa otrzymuje status dead. Endpoint jest automatycznie wyłączany po 20 kolejnych błędach. Odbiorca musi deduplikować po Sablex-Event-Id i szybko zwracać 2xx.