API
Alles, was das Panel kann, steht dir über zwei getrennte APIs zur Verfügung. Beide nutzen dasselbe Konto, dasselbe Guthaben und denselben Katalog; sie unterscheiden sich in Format und Funktionsumfang.
Alles, was das Panel kann, steht dir über zwei getrennte APIs zur Verfügung. Beide nutzen dasselbe Konto, dasselbe Guthaben und denselben Katalog; sie unterscheiden sich in Format und Funktionsumfang.
Warum zwei APIs?
Die klassische Reseller-API (v2), die die ganze Branche nutzt, schickt ein Formular an einen einzigen Endpunkt und antwortet immer mit HTTP 200. Genau diese Form erwartet fertige Panel-Software, deshalb bleibt sie, wie sie ist. Wer eigene Systeme entwickelt, stieß damit aber immer wieder an Grenzen: Fehler ließen sich nicht auseinanderhalten, der Katalog kam in einem Stück, und den Bestellstatus musste man endlos abfragen. Für sie wurde v3 entwickelt.
Vergleich
| Merkmal | Legacy (v2) | Neu (v3) |
|---|---|---|
| Aufbau | Ein Endpunkt, Formular-POST, action-Parameter | Ressourcenorientiertes REST, JSON-Body |
| HTTP-Status | Immer 200, auch bei Fehlern | Echte Codes (400, 401, 402, 404, 409, 429, 502) |
| Fehler | Freitext | type + fester code + lokalisierte message + param + doc_url |
| Bestellstatus | Nur lokalisierter Text | Fester Maschinenwert plus separates Anzeige-Label |
| Servicebeschreibung | Keine | Beschreibung in 16 Sprachen, durchschnittliche Dauer, Plattform, Kategorie |
| Bestellfelder | Aus dem Typnamen erraten | Jeder Service veröffentlicht sein eigenes Feldschema |
| Preiseinheit | Nicht angegeben (bei Paketen eine Fehlerquelle um den Faktor 1.000) | Ausdrücklich per_1000 oder per_order |
| Katalog | Alle Services in einer Antwort | Filter plus Cursor-Paginierung |
| Schutz vor Duplikaten | Keiner | Idempotency-Key |
| Statusupdates | Ständiges Abfragen (Polling) | Signierte Webhooks oder ein Event-Stream |
| Schema | Keines | OpenAPI 3.1 |
| Sprachen | Englisch und Türkisch (getrennte URLs) | 16 Sprachen (per Header oder Parameter) |
| Servicequalität | Keine | Score, Konfidenz und Datenbasis pro Service; sortierte Shortlist |
Welche API soll ich nutzen?
Nimm die Legacy-API, wenn du fertige Panel-Software, einen Bot oder ein Reseller-Panel betreibst. Meist musst du dort nur die API-URL und den Schlüssel ändern, und nach wenigen Minuten läuft alles.
Nimm v3, wenn du deine eigene Anwendung, deinen eigenen Shop oder eine Automatisierung entwickelst. Fehlerbehandlung, Schutz vor doppelten Bestellungen und Benachrichtigungen sind bereits eingebaut, und dein Bestellformular kannst du direkt aus dem Service-Schema erzeugen.
Erste Schritte
- 1Erstelle im Tab „Schlüssel“ einen API-Schlüssel.
- 2Ruf die Serviceliste ab und lies die id und das Feldschema des Service aus, den du brauchst.
- 3Prüf die Bestellung zuerst mit preview und leg sie erst dann an.
- 4Registriere einen Webhook oder lies den Event-Stream, um Statusänderungen zu verfolgen.
Eine REST-API für Entwickler, die eigene Systeme bauen: ressourcenorientierte Pfade, echte HTTP-Statuscodes, maschinenlesbare Fehler und signierte Benachrichtigungen.
Basis-URL
Jeder Pfad wird an diese URL angehängt. Die Version steckt im Pfad: Sollte jemals eine inkompatible Änderung nötig werden, erscheint ein neuer Pfad (v4), und dieser hier läuft unverändert weiter. Das Veröffentlichungsdatum der API-Spezifikation steht in jeder Antwort im Header X-Api-Version.
https://panelfollows.com/api/v3Authentifizierung
Sende deinen API-Schlüssel als Bearer-Token im Header Authorization. Alternativ wird auch der Header X-Api-Key akzeptiert.
GET https://panelfollows.com/api/v3/account
Authorization: Bearer pf_live_...Dein bestehender Legacy-Schlüssel funktioniert auch mit v3, du kannst also sofort loslegen. Im Produktivbetrieb solltest du einen v3-Schlüssel verwenden: Er lässt sich benennen, einzeln widerrufen und wird nie im Klartext gespeichert.
Schnellstart
curl https://panelfollows.com/api/v3/services?limit=5 \
-H "Authorization: Bearer YOUR_API_KEY"Sprache
Die Antwortsprache wählst du über den Header Accept-Language oder den Parameter ?lang=; der Parameter hat Vorrang. Servicenamen, Servicebeschreibungen, Kategorienamen, Status-Labels von Bestellungen, Labels der Bestellfelder und Fehlermeldungen kommen alle in dieser Sprache zurück.
Maschinenwerte ändern sich nie mit der Sprache: error.code, order.status, service.type und currency sind immer gleich. Richte deine Logik nach diesen Werten und zeig deinen Nutzern den Text.
Accept-Language: tr
# veya
GET https://panelfollows.com/api/v3/services?lang=trAnfrage- und Antwortformat
Anfrage-Bodys sind JSON (application/json); für schnelle Tests wird auch form-urlencoded akzeptiert. Antworten sind JSON: Eine einzelne Ressource ist ein einfaches Objekt, Listen kommen in einer Hülle (Envelope) mit data, has_more und next_cursor. Jedes Objekt hat ein Feld object, das seinen Typ nennt.
Beträge sind dezimale STRINGS ("1.2340"), keine Gleitkommazahlen. Wandle sie auf deiner Seite in einen Dezimaltyp um, damit keine Nachkommastellen verloren gehen. Die Währung ist USD.
Zeitstempel folgen RFC 3339 (2026-08-21T00:24:45.255Z).
Fehler
Bei einem Fehler bekommst du einen echten HTTP-Statuscode und einen Body mit genau einem error-Objekt. Dein Code sollte sich nach error.code richten: Dieser Wert ist fest und ändert sich nie mit der Sprache.
HTTP/1.1 400 Bad Request
Content-Type: application/json
X-Request-Id: req_0c858d8af7f65eca001b2f5a
{
"object": "error",
"error": {
"type": "invalid_request_error",
"code": "quantity_out_of_range",
"message": "Miktar, bu servisin izin verdiği aralığın dışında.",
"param": "quantity",
"doc_url": "https://panelfollows.com/api-docs#error-quantity_out_of_range",
"request_id": "req_0c858d8af7f65eca001b2f5a"
}
}| type | Grobe Einordnung: Lohnt sich ein erneuter Versuch, liegt der Fehler bei dir? |
| code | Fester Maschinenwert. Richte deine Logik danach. |
| message | Lesbarer Text in der Sprache, die du gewählt hast. |
| param | Name des fehlerhaften Felds, falls es eins gibt. |
| doc_url | Link zum passenden Abschnitt dieser Dokumentation. |
| request_id | Die Referenz, die du angibst, wenn du dich an den Support wendest. |
Rate-Limits
600 Anfragen pro Minute und Schlüssel, zusätzlich 900 pro Minute und IP. Jede Antwort enthält RateLimit-Limit, RateLimit-Remaining und RateLimit-Reset, damit du dein Tempo drosseln kannst, bevor du an die Grenze stößt. Überschreitest du das Limit, bekommst du 429 mit einem Retry-After-Header.
Paginierung
Listen werden per Cursor paginiert. Sende limit für die Seitengröße (maximal 500) und starting_after mit der id des letzten Eintrags der vorherigen Seite. Mach so lange weiter, bis has_more false ist; next_cursor liefert dir den Cursor für den nächsten Aufruf. Ein limit außerhalb des erlaubten Bereichs wird nicht stillschweigend angepasst, sondern führt zu einem Fehler: Stilles Anpassen lässt Clients glauben, sie hätten alles abgerufen.
Schutz vor Duplikaten (Idempotency-Key)
Füg beim Anlegen einer Bestellung einen zufälligen Idempotency-Key-Header hinzu. Bricht die Verbindung ab und du wiederholst die Anfrage mit demselben Key, entsteht keine zweite Bestellung: Du bekommst die erste Antwort noch einmal, mit dem Header Idempotent-Replay: true. Die Einträge werden 24 Stunden aufbewahrt.
Sendest du denselben Key mit einem ANDEREN Body, kommt 409 idempotency_key_reuse zurück. Das heißt fast immer, dass die Key-Erzeugung auf Client-Seite fehlerhaft ist. Eine fehlgeschlagene Anfrage verbraucht den Key nicht: Behebe das Problem und versuch es mit demselben Key erneut.
Bestellung anlegen
curl -X POST https://panelfollows.com/api/v3/orders \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 8f14e45f-ea3a-4b1c-9c1e-2b0d5c6a7e91" \
-d '{
"service": 1234,
"link": "https://instagram.com/username",
"quantity": 1000
}'Feldschema eines Service
Jeder Service veröffentlicht die Felder, die er braucht, in einem fields-Array: Name, Typ, ob das Feld Pflicht ist, seine Grenzen sowie ein Label und eine Beschreibung in deiner Sprache. Ist bei einem Feld determines_quantity auf true gesetzt, ergibt sich die Menge aus der Anzahl der Zeilen darin.
// Servisin kendi alan şemasından formu OTOMATİK üretmek:
// hiçbir servis tipini koda gömmeniz gerekmez.
const res = await fetch("https://panelfollows.com/api/v3/services/1234", {
headers: { Authorization: "Bearer YOUR_API_KEY", "Accept-Language": "tr" },
});
const service = await res.json();
for (const field of service.fields) {
renderInput({
name: field.name,
label: field.label, // kullanıcının dilinde
hint: field.description, // kullanıcının dilinde
required: field.required,
type: field.type, // url | integer | string | text_lines
min: field.min,
max: field.max,
// true ise miktarı bu alanın satır sayısı belirler
countsLines: field.determines_quantity === true,
});
}Das Service-Objekt
{
"object": "service",
"id": 1234,
"name": "Instagram Takipçi | Türk | 30 gün telafi",
"description": "Gerçek hesaplardan Türk takipçi. Başlangıç 0-1 saat.",
"type": "default",
"platform": "instagram",
"category": { "slug": "instagram-takipci", "name": "Instagram Takipçi" },
"pricing": {
"rate": "1.2340",
"currency": "USD",
"unit": "per_1000",
"unit_note": "Fiyat 1000 adet içindir."
},
"limits": { "min": 100, "max": 100000 },
"features": { "refill": true, "cancel": false, "dripfeed": true },
"average_time_seconds": 4320,
"fields": [
{
"name": "link",
"type": "url",
"required": true,
"label": "Bağlantı",
"description": "Gönderimin yapılacağı profilin herkese açık adresi."
},
{
"name": "quantity",
"type": "integer",
"required": true,
"label": "Miktar",
"description": "Kaç adet gönderileceği.",
"min": 100,
"max": 100000
}
],
"is_active": true,
"updated_at": "2026-08-20T09:15:00.000Z"
}Bestellvorschau (Probelauf)
POST /orders/preview prüft eine Bestellung und berechnet den Preis, OHNE sie anzulegen. So kannst du deinem Kunden vorab den Preis zeigen und prüfen, ob dein Guthaben reicht. Es wird nichts abgebucht und kein Anbieter kontaktiert.
Sammelbestellungen
POST /orders/batch nimmt bis zu 50 Bestellungen in einem Aufruf an. Die Einträge werden der Reihe nach verarbeitet, und jeder meldet sein eigenes Ergebnis: Schlägt einer fehl, werden die übrigen trotzdem angelegt, und du siehst genau, welcher gescheitert ist und warum.
Verknüpfte Objekte einbetten
Übergib include=service an die Bestell-Endpunkte, dann wird das Service-Objekt direkt in die Antwort eingebettet und du sparst dir eine zweite Anfrage.
Servicequalität und die Shortlist der besten Services
Das Panel misst jeden Service stündlich: wie unsere eigenen Bestellungen dafür ausgegangen sind (abgeschlossen, storniert, hängen geblieben, abgelehnt), wie oft Kunden ein Refill angefordert oder ein Ticket eröffnet haben, wie lange die Lieferung tatsächlich gedauert hat und ob die Quelle den Service noch führt. GET /services/top macht aus diesen Messungen eine sortierte Shortlist, und include=quality hängt denselben Bericht an jedes Service-Objekt an.
Der Score (0-100) ist eine gewichtete Mischung aus fünf Komponenten: Zuverlässigkeit (42 %, die geschätzte Erfolgsquote), Zufriedenheit (14 %, aus der Beschwerdequote), Tempo (28 %, logarithmisch abhängig von der effektiven Lieferzeit; 5 Minuten ergeben die volle Punktzahl, 48 Stunden null), der eigene Score der Health-Engine (8 %) und der Umfang der Datenbasis (8 %). Kleine Boni gibt es für eine Refill-Garantie, eine vertrauenswürdige Quelle und eine lange Zugehörigkeit zum Katalog. Services, die gerade geprüft werden, von der Health-Engine markiert wurden oder von einer Quelle in der Probezeit stammen, bekommen zwar einen Score, landen aber nie im Ranking.
Filtere nach Plattform, Kategorie-Slug oder Service-Art (shelf: followers, likes, views …, plattformübergreifend), sortiere nach score, speed, reliability, price oder orders und nutze group_by=category (oder platform) mit per_group, um in einem einzigen Aufruf eine Shortlist pro Kategorie zu bekommen: genau das, was ein Shop braucht, um in jeder Kategorie einen „empfohlenen“ Service zu markieren.
# Instagram takipçi kategorisinde en iyi 5 servis
curl "https://panelfollows.com/api/v3/services/top?category=instagram-followers&limit=5" -H "Authorization: Bearer YOUR_API_KEY"
# Her kategori için kanıtlı en iyi 3 servis (tek istek)
curl "https://panelfollows.com/api/v3/services/top?group_by=category&per_group=3&min_confidence=medium" -H "Authorization: Bearer YOUR_API_KEY"| score / grade | 0-100 und A (85+), B (70+), C (55+), D. |
| confidence | none, low, medium, high: wie viele unserer eigenen Bestellungen den Score stützen. |
| badges | proven (5+ Bestellungen, Untergrenze der Erfolgsquote 60 %+), popular (20+ Bestellungen), fast (Lieferung innerhalb von 1 Stunde), trusted_source, new (jünger als 14 Tage). |
| components | reliability, satisfaction, speed, health, evidence, jeweils 0-1. |
| evidence.basis | service_orders (eigene Bestellungen), peer_services (Startwert aus den anderen Services der Quelle) oder none. |
| evidence.delivery_source | measured (unser eigener Median über 3+ abgeschlossene Bestellungen) oder claimed (die von der Quelle angegebene Zeit, mit Abschlag und Obergrenze). |
| measured_at | Zeitpunkt, zu dem die Health-Engine den Service zuletzt gemessen hat; Scores werden stündlich aktualisiert. |
Endpunkt-Referenz
| Methode | Endpunkt | Beschreibung |
|---|---|---|
| GET | /api/v3 | Discovery document: version, endpoints, limits and event types. |
| GET | /api/v3/openapi.json | OpenAPI 3.1 schema for this API. |
| GET | /api/v3/account | Account balance, currency and current rate-limit window. |
| PATCH | /api/v3/account | Set the low balance alert threshold that triggers account.low_balance. |
| GET | /api/v3/services | List services with filters and cursor pagination. |
| GET | /api/v3/services/top | Best services ranked by the quality score; optionally one shortlist per category or platform. |
| GET | /api/v3/services/{id} | Retrieve one service, including its order field schema. |
| GET | /api/v3/categories | List categories with platform, shelf and active service counts. |
| GET | /api/v3/platforms | List platform keys usable as the ?platform= filter, with counts. |
| POST | /api/v3/orders | Create an order. Supports the Idempotency-Key header. |
| POST | /api/v3/orders/preview | Validate an order and compute its charge without creating it. |
| POST | /api/v3/orders/batch | Create up to 50 orders in one call; each item reports its own result. |
| GET | /api/v3/orders | List your orders, newest first. |
| GET | /api/v3/orders/{id} | Retrieve one order. |
| POST | /api/v3/orders/{id}/cancel | Request cancellation. Only for services whose features.cancel is true. |
| POST | /api/v3/orders/{id}/refill | Request a refill for a completed order. |
| GET | /api/v3/refills | List your refill requests, newest first. |
| GET | /api/v3/refills/{id} | Retrieve one refill; refreshes its status from the provider. |
| GET | /api/v3/events | Read your event stream oldest-first; the polling alternative to webhooks. |
| GET | /api/v3/webhooks | List your webhook endpoints. |
| POST | /api/v3/webhooks | Register a webhook endpoint. The signing secret is returned once. |
| GET | /api/v3/webhooks/{id} | Retrieve one webhook endpoint. |
| PATCH | /api/v3/webhooks/{id} | Update a webhook endpoint's url, events, description or active state. |
| DELETE | /api/v3/webhooks/{id} | Delete a webhook endpoint and its delivery log. |
| POST | /api/v3/webhooks/{id}/test | Send a test event to this endpoint, ignoring its event filter. |
| POST | /api/v3/webhooks/{id}/rotate_secret | Generate a new signing secret. The old one stops working immediately. |
| GET | /api/v3/webhooks/{id}/deliveries | Delivery log for one endpoint: attempts, response codes and errors. |
OpenAPI-Schema
Eine maschinenlesbare Beschreibung aller Endpunkte. Gib diese Datei an einen Client-Generator (openapi-generator, Kiota) oder an Postman weiter und du bekommst einen fertigen Client in deiner eigenen Programmiersprache.
https://panelfollows.com/api/v3/openapi.jsonHäufige Fragen
Funktioniert mein Legacy-Schlüssel mit v3?
Ja. Zum Ausprobieren brauchst du keinen neuen Schlüssel. Im Produktivbetrieb solltest du trotzdem auf einen v3-Schlüssel umsteigen: Er lässt sich widerrufen und wird nicht im Klartext gespeichert.
Warum sind Preise Strings?
Gleitkommazahlen verlieren bei Dezimalbeträgen Nachkommastellen. Wenn wir Strings liefern und du sie auf deiner Seite in einen Dezimaltyp umwandelst, fällt diese ganze Klasse von Rundungsdifferenzen weg.
Warum ist das Feld unit wichtig?
Die meisten Services werden pro 1.000 Einheiten abgerechnet (per_1000), Paket-Services dagegen als einzelner Artikel (per_order), bei dem der Preis das ganze Paket abdeckt. Integrationen, die diesen Unterschied ignoriert haben, berechneten Paketpreise um den Faktor 1.000 falsch.
Meine Bestellung wurde angelegt, hängt aber auf pending fest. Was ist passiert?
Die Übergabe an den Anbieter hat sich möglicherweise verzögert. Dein Guthaben ist reserviert und die Bestellung geht nicht verloren; unser Team sendet sie automatisch erneut. Das Feld processing_delayed kennzeichnet diesen Zustand.
Unterstützen alle Services Stornierung und Refill?
Nein. Prüf features.cancel und features.refill im Service-Objekt. Rufst du den Endpunkt für einen Service auf, der das nicht unterstützt, bekommst du 400 zurück.
Kann ich beide APIs gleichzeitig nutzen?
Ja. Gleiches Konto, gleiches Guthaben, gleiche Bestellungen. Eine Bestellung, die du über v2 aufgegeben hast, kannst du über v3 abrufen.
Die klassische Reseller-API, die in der ganzen Branche Standard ist. Genau diese Form erwartet fertige Panel-Software.
Endpunkt
POST https://panelfollows.com/api/v2
POST https://panelfollows.com/api/v2/trAuthentifizierung
Jede Anfrage enthält den Parameter key. Halte deinen Schlüssel geheim und erzeuge sofort einen neuen, falls er nach außen gelangt.
Anfrage- und Antwortformat
Anfragen werden als Formular per POST gesendet (application/x-www-form-urlencoded), Antworten kommen als JSON. Auch Fehler liefern HTTP 200, mit { "error": "..." } im Body.
Ebenfalls unterstützt, auch wenn es bisher nirgends dokumentiert war: Du kannst die API per GET aufrufen und den Body als application/json senden.
Rate-Limits
240 Anfragen pro Minute und Schlüssel, zusätzlich 300 pro Minute und IP. Überzählige Anfragen werden mit 429 abgelehnt.
Aktionen und Parameter
| action | Parameter | Beschreibung |
|---|---|---|
| services | key, action | Listet alle aktiven Services auf (id, Name, Kategorie, Preis, Min./Max., Refill, Stornierung, Drip-Feed). |
| add | key, action, service, link, quantity[, runs, interval, comments, username, posts, min, max] | Legt eine Bestellung an. service ist die Service-ID aus dem Katalog. Für Drip-Feed ergänzt du runs und interval, für Sondertypen die passenden Felder. |
| status | key, action, order | orders | Bestellstatus. Nutze order für eine einzelne Bestellung oder eine kommagetrennte orders-Liste für mehrere. |
| balance | key, action | Guthaben und Währung des Kontos. |
| refill | key, action, order | orders | Erstellt eine Refill-Anfrage, die direkt an den Anbieter geht. |
| refill_status | key, action, refill | refills | Fragt den Status von Refills ab. |
| cancel | key, action, orders | Storniert Bestellungen. Funktioniert nur bei Services, deren Anbieter Stornierungen unterstützt. |
Beispiel
curl -X POST https://panelfollows.com/api/v2 \
-d "key=YOUR_API_KEY" \
-d "action=add" \
-d "service=1234" \
-d "link=https://instagram.com/username" \
-d "quantity=1000"
# Yanıt: { "order": 23501 }Antworten auf Türkisch
Häng /tr an die URL an, um Servicenamen, Kategorien, Bestellstatus und Fehlermeldungen auf Türkisch zu erhalten. Parameter, Aktionen und Antwortstruktur sind identisch, und dein Schlüssel funktioniert unter beiden URLs. Technische Felder (type, refill_status, currency) bleiben für die Kompatibilität mit dem Standard auf Englisch.
Umstieg auf v3
Der Umstieg ist freiwillig. Wenn du wechselst, bleibt der Großteil deiner Geschäftslogik erhalten, weil die Parameternamen gleich bleiben; anders sind nur der Transport und die Art, wie du Fehler ausliest.
- 1Verschiebe den Schlüssel aus dem Feld key im Body in den Header Authorization: Bearer.
- 2Ruf einen Ressourcenpfad auf statt action=... (POST /orders statt add).
- 3Erkenne Fehler am HTTP-Status und an error.code statt an der Frage „Gibt es ein error-Feld?“.
- 4Vergleiche den Bestellstatus mit dem Maschinenwert, nicht mit dem Anzeigetext.
- 5Füg beim Anlegen von Bestellungen einen Idempotency-Key hinzu.
- 6Ersetze das ständige Abfragen des Status durch Webhooks.
Ein Schlüssel gewährt vollen Zugriff auf dein Konto. Gib ihn nicht weiter, bau ihn nicht in clientseitigen Code ein und committe ihn niemals in ein öffentliches Repository.
v3-Schlüssel
Erstelle so viele Schlüssel, wie du brauchst, gib jedem eine Bezeichnung und widerrufe sie einzeln. Bei uns wird nur ein kryptografischer Hash des Schlüssels gespeichert.
Kostenloses Konto erstellenLegacy-Schlüssel
Der einzelne Schlüssel der klassischen Reseller-API (v2). Er funktioniert auch mit v3. Wenn du ihn neu erzeugst, wird der alte Wert sofort ungültig.
Sicherheit
- Speichere den Schlüssel in einer Umgebungsvariable, nie im Quellcode.
- Nutze keinen Schlüssel in Code, der im Browser läuft; leite die Aufrufe über deinen eigenen Server.
- Erstelle für jedes System einen eigenen Schlüssel, damit das Widerrufen eines Schlüssels die anderen nicht betrifft.
- Wenn du ein Leck vermutest, rolle zuerst den neuen Schlüssel aus und widerrufe erst dann den alten.
Ändert sich der Status einer Bestellung, schicken wir eine signierte Benachrichtigung an deinen Server. So musst du den Status nie selbst abfragen.
Warum Webhooks?
Polling ist langsam und verschwenderisch zugleich: Fragst du jede Minute Tausende Bestellungen ab, verbrauchst du dein Rate-Limit und erfährst von Änderungen trotzdem erst Minuten später. Mit Webhooks erreicht dich eine Änderung in dem Moment, in dem sie passiert.
Einrichtung
- 1Bereite eine öffentliche https-URL vor (lokale Adressen und Adressen aus privaten Netzen werden abgelehnt).
- 2Füg die URL unten hinzu und speichere das Signatur-Secret, das nur einmal angezeigt wird.
- 3Prüf die Signatur auf deiner Seite und antworte mit 2xx.
- 4Nutze den Test-Button, um den gesamten Weg von Anfang bis Ende zu prüfen.
Was wir senden
POST /hooks/pf HTTP/1.1
Content-Type: application/json
Webhook-Id: evt_7f910fba7cd042ef9d9069ba5c074fa0
Webhook-Timestamp: 1787261223
Webhook-Signature: t=1787261223,v1=9c1e2b0d5c6a7e91...
{
"object": "event",
"id": "evt_7f910fba7cd042ef9d9069ba5c074fa0",
"type": "order.completed",
"created_at": "2026-08-21T00:27:03.531Z",
"data": {
"previous_status": "in_progress",
"order": {
"object": "order",
"id": 23501,
"status": "completed",
"status_label": "Tamamlandı",
"service": 1234,
"quantity": 1000,
"start_count": 4210,
"remains": 0,
"charge": "1.2340",
"currency": "USD"
}
}
}Signatur prüfen
Jede Anfrage enthält einen Webhook-Signature-Header: t ist der Zeitstempel, v1 die Signatur. Die Signatur ist der HMAC-SHA256 des Strings "<timestamp>.<raw body>", berechnet mit deinem Secret.
- 1Lies t und v1 aus dem Header aus.
- 2Prüf, dass t nicht älter als 5 Minuten ist, um Replay-Angriffe abzuwehren.
- 3Berechne den HMAC-SHA256 von "<t>.<raw body>" mit deinem Secret.
- 4Vergleiche das Ergebnis in konstanter Zeit mit v1 und lehne die Anfrage ab, wenn es nicht übereinstimmt.
Beispiel zur Prüfung
import crypto from "node:crypto";
import express from "express";
const app = express();
// ÖNEMLİ: imza HAM gövde üzerinden hesaplanır. JSON'a çevirip yeniden
// dizeye dönüştürürseniz boşluklar değişir ve imza tutmaz.
app.post("/hooks/pf", express.raw({ type: "application/json" }), (req, res) => {
const raw = req.body.toString("utf8");
const header = req.get("Webhook-Signature") ?? "";
const m = /t=(\d+),v1=([0-9a-f]+)/.exec(header);
if (!m) return res.sendStatus(400);
const [, timestamp, signature] = m;
// Tekrar saldırısına karşı: 5 dakikadan eski damgayı reddet.
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return res.sendStatus(400);
const expected = crypto
.createHmac("sha256", process.env.WEBHOOK_SECRET)
.update(`${timestamp}.${raw}`, "utf8")
.digest("hex");
const ok =
expected.length === signature.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
if (!ok) return res.sendStatus(401);
const event = JSON.parse(raw);
// 2xx dönmezseniz gönderim artan aralıklarla tekrar denenir.
res.sendStatus(200);
if (event.type === "order.completed") {
// ... siparişi kendi sisteminizde tamamlandı olarak işaretleyin
}
});Wiederholungen
Der erste Versuch erfolgt, sobald das Ereignis eintritt. Ist die Antwort kein 2xx oder schlägt die Verbindung fehl, wird die Zustellung nach 1 Minute, 5 Minuten, 30 Minuten, 2 Stunden und 6 Stunden wiederholt. Nach 6 Versuchen gilt die Zustellung als fehlgeschlagen und erscheint im Zustellprotokoll.
Ereignistypen
Abonniere die Typen, die dich interessieren, oder empfange alle. Eine Statusänderung erzeugt genau ein Ereignis, und zwar mit dem Typ, der am besten zum neuen Status passt.
| order.created | Eine Bestellung wurde angelegt. |
| order.processing | Der Anbieter hat mit der Bearbeitung der Bestellung begonnen. |
| order.completed | Die Bestellung ist abgeschlossen. |
| order.partial | Die Bestellung wurde teilweise geliefert, der Rest wurde erstattet. |
| order.canceled | Die Bestellung wurde storniert oder erstattet. |
| order.updated | Der Status hat sich auf andere Weise geändert. |
| refill.created | Ein Refill wurde angefordert. |
| refill.updated | Der Status eines Refills hat sich geändert. |
| account.low_balance | Dein Guthaben ist unter den Schwellenwert gefallen, den du mit PATCH /account festgelegt hast. Wird beim Unterschreiten ausgelöst, nicht bei jeder Bestellung, und ist wieder scharf geschaltet, sobald das Guthaben wieder darüber liegt. |
Wenn du keinen Webhook hosten kannst
Dieselben Ereignisse kannst du per Cursor über GET /api/v3/events abrufen. Nutze das bei der lokalen Entwicklung, wenn du keine statische IP hast oder hinter einer Firewall sitzt.
// Webhook kuramıyorsanız (yerelde geliştirme, sabit IP yok) aynı bilgiyi
// imleçle çekebilirsiniz. İmleci kendi tarafınızda saklayın.
let cursor = loadCursor(); // en son işlediğiniz olayın "cursor" değeri
const url = new URL("https://panelfollows.com/api/v3/events");
url.searchParams.set("limit", "100");
if (cursor) url.searchParams.set("starting_after", String(cursor));
const res = await fetch(url, { headers: { Authorization: "Bearer YOUR_API_KEY" } });
const { data, has_more } = await res.json();
for (const event of data) {
handle(event); // sizin işleyiciniz
cursor = event.cursor; // imleci ilerlet
}
saveCursor(cursor);Fehler (48)
| Code | Status | Beschreibung |
|---|---|---|
| missing_api_key | 401 | Kein API-Schlüssel angegeben. Sende ihn als 'Authorization: Bearer <key>'. |
| invalid_api_key | 401 | Der angegebene API-Schlüssel ist ungültig. |
| revoked_api_key | 401 | Dieser API-Schlüssel wurde widerrufen und kann nicht mehr verwendet werden. |
| account_banned | 403 | Dieses Konto ist gesperrt. |
| account_suspended | 403 | Dieses Konto ist derzeit gesperrt. |
| insufficient_scope | 403 | Dieser API-Schlüssel hat keine Berechtigung für diesen Endpunkt. |
| invalid_json | 400 | Der Anfrage-Body ist kein gültiges JSON. |
| unsupported_content_type | 415 | Nicht unterstützter Content-Type. Verwende application/json oder application/x-www-form-urlencoded. |
| method_not_allowed | 405 | Diese HTTP-Methode ist für diesen Endpunkt nicht erlaubt. |
| payload_too_large | 413 | Der Anfrage-Body ist zu groß. |
| missing_parameter | 400 | Ein erforderlicher Parameter fehlt. |
| invalid_parameter | 400 | Ein Parameter hat einen ungültigen Wert. |
| invalid_link | 400 | Der Link fehlt oder ist keine gültige http(s)-URL. |
| invalid_quantity | 400 | Die Menge ist keine gültige positive ganze Zahl. |
| quantity_out_of_range | 400 | Die Menge liegt außerhalb des Bereichs, den dieser Service erlaubt. |
| invalid_comments | 400 | Das Feld comments ist leer oder hat zu viele Zeilen. |
| invalid_username | 400 | Der Benutzername ist für diesen Service ungültig. |
| invalid_subscription | 400 | Die Abo-Parameter sind ungültig. |
| invalid_runs | 400 | Der Wert für 'runs' ist für Drip-Feed ungültig. |
| invalid_interval | 400 | Der Wert für 'interval' ist für Drip-Feed ungültig. |
| dripfeed_not_supported | 400 | Dieser Service unterstützt kein Drip-Feed. |
| missing_required_field | 400 | Ein Feld, das dieser Servicetyp verlangt, fehlt oder ist ungültig. |
| service_inactive | 400 | Dieser Service kann derzeit nicht bestellt werden. |
| invalid_cursor | 400 | Der Paginierungs-Cursor ist ungültig. |
| invalid_limit | 400 | Der Parameter 'limit' liegt außerhalb des erlaubten Bereichs. |
| invalid_webhook_url | 400 | Die Webhook-URL muss eine öffentliche https://-Adresse sein. |
| invalid_events | 400 | Mindestens einer der angegebenen Ereignistypen ist unbekannt. |
| batch_too_large | 400 | Zu viele Einträge in einer einzelnen Batch-Anfrage. |
| cancel_not_supported | 400 | Dieser Service unterstützt keine Stornierung. |
| refill_not_supported | 400 | Dieser Service bietet kein Refill an. |
| unknown_endpoint | 404 | Unbekannter Endpunkt. Die verfügbaren Routen findest du in der API-Referenz. |
| service_not_found | 404 | Es gibt keinen Service mit dieser ID. |
| order_not_found | 404 | In deinem Konto gibt es keine Bestellung mit dieser ID. |
| refill_not_found | 404 | In deinem Konto gibt es kein Refill mit dieser ID. |
| webhook_not_found | 404 | In deinem Konto gibt es keinen Webhook-Endpunkt mit dieser ID. |
| order_not_cancelable | 409 | Diese Bestellung kann aufgrund ihres aktuellen Status nicht mehr storniert werden. |
| cancel_rejected | 409 | Der Anbieter hat die Stornierungsanfrage abgelehnt. |
| order_not_completed | 409 | Ein Refill kann nur für eine abgeschlossene Bestellung angefordert werden. |
| duplicate_link | 409 | Für diesen Link gibt es bereits eine aktive Bestellung. Warte, bis sie abgeschlossen ist. |
| idempotency_key_reuse | 409 | Dieser Idempotency-Key wurde bereits mit einem anderen Anfrage-Body verwendet. |
| idempotency_in_progress | 409 | Eine Anfrage mit diesem Idempotency-Key wird noch verarbeitet. Versuch es gleich noch einmal. |
| webhook_limit_reached | 409 | Du hast die maximale Anzahl an Webhook-Endpunkten erreicht. |
| insufficient_balance | 402 | Nicht genügend Guthaben für diese Bestellung. |
| rate_limit_exceeded | 429 | Rate-Limit überschritten. Beachte den Antwort-Header Retry-After. |
| provider_error | 502 | Der vorgelagerte Anbieter hat einen Fehler gemeldet. Versuch es erneut. |
| refill_failed | 502 | Der Anbieter hat die Refill-Anfrage abgelehnt. |
| service_temporarily_unavailable | 503 | Dieser Service ist vorübergehend nicht verfügbar. Versuch es später erneut. |
| internal_error | 500 | Auf unserer Seite ist ein unerwarteter Fehler aufgetreten. |