Public API v1

Integracja Sablex: test, live i webhooks.

Bazowy URL: https://sablex.pl/api/v1. Cena, rabat 5%, bezpieczeństwo marży i saldo są liczone wyłącznie na serwerze.

Tryb testowy

Bez salda i bez dostawcy, z pełną walidacją.

IP allowlist

IPv4, IPv6 i CIDR osobno dla każdego klucza.

HMAC webhooks

Kolejka, osiem prób i odporność na duplikaty.

Środowiska test i live

Prefiks klucza określa środowisko na stałe. Środowiska mają osobne zamówienia, odczyt statusu i idempotency.

Test

sblx_test_

Brak pobrania salda

Brak wysyłki do dostawcy

Numer TST-*

Pełna walidacja i cena

Live

sblx_live_

Atomowe pobranie z salda

Automatyczna realizacja

Numer SAB-*

Prawdziwe statusy dostawcy

Autoryzacja i ograniczenie IP

Najbezpieczniejszy sposób autoryzacji:

Authorization: Bearer sblx_test_xxxxxxxxxxxxxxxxx

Każ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
Pełny sekret klucza jest pokazywany tylko raz. Klient spoza allowlisty otrzymuje 403 ip_not_allowed przed wykonaniem operacji.

GET /services

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.

POST /orders i idempotency

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

Gotowe przykłady PHP, Python i JavaScript

PHP
<?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']);
Python
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"])
JavaScript
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);

Format błędów

{
  "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"
    }
  }
}
HTTP 401invalid_api_key
HTTP 402insufficient_balance
HTTP 403ip_not_allowed / insufficient_scope
HTTP 409idempotency_conflict / external_order_exists
HTTP 425idempotency_in_progress
HTTP 429rate_limit_exceeded

Podpisane webhooks

Konfiguracja znajduje się w panelu API. Obsługiwane zdarzenia:

order.createdorder.processingorder.completedorder.failedorder.refunded
Sablex-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
    }
  }
}
Payload nie zawiera supplier ID, odpowiedzi dostawcy, kosztu zakupu ani marży Sablex.

Weryfikacja HMAC-SHA256

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
<?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';
Python
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", 200
JavaScript
import 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");
  }
);

Kolejka i ponowienia

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 dni

Po ó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.