API
Tout ce que fait le panel est accessible via deux API distinctes. Elles partagent le même compte, le même solde et le même catalogue, et diffèrent par leur format et leurs fonctionnalités.
Tout ce que fait le panel est accessible via deux API distinctes. Elles partagent le même compte, le même solde et le même catalogue, et diffèrent par leur format et leurs fonctionnalités.
Pourquoi deux API ?
L’API revendeur classique (v2), utilisée par tout le secteur, reçoit un formulaire envoyé en POST sur un endpoint unique et répond toujours avec un code HTTP 200. C’est exactement le format qu’attendent les logiciels de panel prêts à l’emploi, elle reste donc telle quelle. Les développeurs qui écrivent leurs propres systèmes se heurtaient sans cesse à ses limites : impossible de distinguer les erreurs, le catalogue arrivait d’un seul bloc et il fallait interroger le statut des commandes en permanence. La v3 a été conçue pour eux.
Comparatif
| Fonctionnalité | Classique (v2) | Nouvelle (v3) |
|---|---|---|
| Structure | Un seul endpoint, envoi de formulaire, paramètre action | REST orienté ressources, corps JSON |
| Statut HTTP | Toujours 200, même en cas d’échec | Vrais codes (400, 401, 402, 404, 409, 429, 502) |
| Erreurs | Texte libre | type + code stable + message localisé + param + doc_url |
| Statut de commande | Texte localisé uniquement | Valeur machine stable, plus un libellé d’affichage distinct |
| Description du service | Aucune | Description en 16 langues, délai moyen, plateforme, catégorie |
| Champs de commande | Devinés à partir du nom du type | Chaque service publie son propre schéma de champs |
| Unité de prix | Non précisée (source d’erreurs d’un facteur 1 000 sur les forfaits) | Explicitement per_1000 ou per_order |
| Catalogue | Tous les services dans une seule réponse | Filtres et pagination par curseur |
| Protection contre les doublons | Aucune | Idempotency-Key |
| Suivi des statuts | Interrogation permanente (polling) | Webhooks signés ou flux d’événements |
| Schéma | Aucun | OpenAPI 3.1 |
| Langues | Anglais et turc (URL distinctes) | 16 langues (en-tête ou paramètre) |
| Qualité des services | Aucune | Score, niveau de confiance et éléments de preuve par service, sélection classée des meilleurs |
Laquelle choisir ?
Choisissez l’API classique si vous utilisez un logiciel de panel prêt à l’emploi, un bot ou un panel revendeur. La plupart ne vous demandent que de changer l’URL de l’API et la clé, et fonctionnent en quelques minutes.
Choisissez la v3 si vous développez votre propre application, boutique en ligne ou automatisation. La gestion des erreurs, la protection contre les doublons et les notifications sont intégrées, et vous pouvez générer votre formulaire de commande directement à partir du schéma du service.
Premiers pas
- 1Créez une clé API dans l’onglet Clés.
- 2Récupérez la liste des services et relevez l’id et le schéma de champs du service dont vous avez besoin.
- 3Validez d’abord la commande avec preview, puis créez-la.
- 4Enregistrez un webhook, ou lisez le flux d’événements, pour suivre les changements de statut.
Une API REST conçue pour les développeurs qui construisent leurs propres systèmes : chemins orientés ressources, vrais codes de statut HTTP, erreurs lisibles par machine et notifications signées.
URL de base
Chaque chemin s’ajoute à cette URL. La version figure dans le chemin : si un changement incompatible devient un jour nécessaire, un nouveau chemin (v4) sera publié et celui-ci continuera de fonctionner sans aucune modification. La date de publication du contrat est renvoyée dans l’en-tête X-Api-Version de chaque réponse.
https://panelfollows.com/api/v3Authentification
Envoyez votre clé API comme jeton Bearer dans l’en-tête Authorization. L’en-tête X-Api-Key est également accepté comme alternative.
GET https://panelfollows.com/api/v3/account
Authorization: Bearer pf_live_...Votre clé classique existante fonctionne aussi sur la v3, vous pouvez donc l’essayer tout de suite. En production, utilisez une clé v3 : elle peut porter un libellé, être révoquée individuellement et n’est jamais stockée en clair.
Démarrage rapide
curl https://panelfollows.com/api/v3/services?limit=5 \
-H "Authorization: Bearer YOUR_API_KEY"Langue
Choisissez la langue des réponses avec l’en-tête Accept-Language ou le paramètre ?lang= (le paramètre est prioritaire). Les noms et descriptions des services, les noms de catégories, les libellés de statut des commandes, les libellés des champs de commande et les messages d’erreur sont alors renvoyés dans cette langue.
Les valeurs machine ne changent jamais selon la langue : error.code, order.status, service.type et currency restent toujours identiques. Basez votre logique sur ces valeurs et affichez le texte à vos utilisateurs.
Accept-Language: tr
# veya
GET https://panelfollows.com/api/v3/services?lang=trFormat des requêtes et des réponses
Le corps des requêtes est en JSON (application/json) ; le format form-urlencoded est aussi accepté pour les tests rapides. Les réponses sont en JSON : une ressource unique est un objet simple, et les listes arrivent dans une enveloppe contenant data, has_more et next_cursor. Chaque objet porte un champ object qui indique son type.
Les montants sont des CHAÎNES décimales ("1.2340"), pas des nombres à virgule flottante. Convertissez-les en type décimal de votre côté pour ne perdre aucune fraction. Tous les montants sont en USD.
Les horodatages sont au format RFC 3339 (2026-08-21T00:24:45.255Z).
Erreurs
En cas d’échec, l’API renvoie un vrai code de statut HTTP et un corps contenant un unique objet error. Votre code doit se baser sur error.code : cette valeur est stable et ne change jamais selon la langue.
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 | Catégorie générale : indique si la requête peut être relancée et si l’erreur vient de vous. |
| code | Valeur machine stable. Basez votre logique dessus. |
| message | Texte lisible dans la langue que vous avez choisie. |
| param | Nom du champ en cause, le cas échéant. |
| doc_url | Lien vers la section exacte de cette documentation. |
| request_id | La seule référence à communiquer lorsque vous contactez l’assistance. |
Limites de requêtes
600 requêtes par minute et par clé, plus 900 par minute et par IP. Chaque réponse contient RateLimit-Limit, RateLimit-Remaining et RateLimit-Reset, ce qui vous permet de ralentir avant d’atteindre la limite. Au-delà, l’API renvoie 429 avec un en-tête Retry-After.
Pagination
Les listes sont paginées par curseur. Envoyez limit pour la taille de page (500 maximum) et starting_after avec l’id du dernier élément de la page précédente. Continuez jusqu’à ce que has_more vaille false ; next_cursor vous donne le curseur de l’appel suivant. Une valeur de limit hors plage n’est pas ramenée silencieusement dans les bornes, elle provoque une erreur : un plafonnement silencieux ferait croire au client qu’il a tout récupéré.
Protection contre les doublons (Idempotency-Key)
Ajoutez un en-tête Idempotency-Key aléatoire lorsque vous créez une commande. Si la connexion est coupée et que vous réessayez avec la même clé, aucune deuxième commande n’est créée : la première réponse est renvoyée à nouveau, avec l’en-tête Idempotent-Replay: true. Les enregistrements sont conservés 24 heures.
Envoyer la même clé avec un corps DIFFÉRENT renvoie 409 idempotency_key_reuse. Cela signifie presque toujours que la génération des clés est défaillante côté client. Une requête échouée ne consomme pas la clé : corrigez le problème et réessayez avec la même.
Créer une commande
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
}'Schéma des champs d’un service
Chaque service publie les champs dont il a besoin dans un tableau fields : nom, type, caractère obligatoire ou non, limites, ainsi qu’un libellé et une description dans votre langue. Lorsqu’un champ a determines_quantity à true, la quantité est déduite du nombre de lignes qu’il contient.
// 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,
});
}L’objet service
{
"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"
}Aperçu de commande (simulation)
POST /orders/preview valide une commande et calcule son coût SANS la créer. Affichez le prix à votre client et vérifiez à l’avance que votre solde suffit. Rien n’est débité et aucun fournisseur n’est contacté.
Commandes groupées
POST /orders/batch accepte jusqu’à 50 commandes en un seul appel. Les éléments sont traités dans l’ordre et chacun renvoie son propre résultat : si l’un échoue, les autres sont tout de même créés, et vous savez exactement lequel a échoué et pourquoi.
Inclure les objets liés
Passez include=service sur les endpoints de commande : l’objet service est intégré à la réponse, ce qui vous évite une seconde requête.
Qualité des services et sélection des meilleurs
Toutes les heures, le panel évalue chaque service : comment ont abouti nos propres commandes (terminées, annulées, bloquées, rejetées), à quelle fréquence les clients ont demandé un remplacement ou ouvert un ticket, combien de temps la livraison a réellement pris, et si la source le propose toujours. GET /services/top transforme ces mesures en une sélection classée, et include=quality ajoute le même rapport à n’importe quel objet service.
Le score (0-100) est une combinaison pondérée de cinq composantes : fiabilité (42 %, le taux de réussite estimé), satisfaction (14 %, d’après le taux de réclamations), vitesse (28 %, logarithmique selon le délai de livraison effectif ; 5 minutes donnent la note maximale, 48 heures donnent zéro), le score propre du moteur de santé (8 %) et la quantité de preuves (8 %). De petits bonus récompensent une garantie de remplacement, une source de confiance et l’ancienneté dans le catalogue. Les services en cours d’examen, signalés par le moteur de santé ou provenant d’une source en période probatoire sont notés, mais jamais classés.
Filtrez par plateforme, slug de catégorie ou rayon (followers, likes, views… communs à toutes les plateformes), triez par score, vitesse, fiabilité, prix ou nombre de commandes, et utilisez group_by=category (ou platform) avec per_group pour obtenir une sélection par catégorie en un seul appel : exactement ce qu’il faut à une boutique pour mettre en avant un service « recommandé » dans chaque catégorie.
# 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 et A (85+), B (70+), C (55+), D. |
| confidence | none, low, medium, high : le nombre de nos propres commandes sur lesquelles repose le score. |
| badges | proven (5+ commandes, borne inférieure du taux de réussite de 60 % ou plus), popular (20+ commandes), fast (livraison en 1 heure maximum), trusted_source, new (moins de 14 jours). |
| components | reliability, satisfaction, speed, health, evidence, chacun entre 0 et 1. |
| evidence.basis | service_orders (ses propres commandes), peer_services (point de départ tiré des autres services de la source) ou none. |
| evidence.delivery_source | measured (notre propre médiane sur au moins 3 commandes terminées) ou claimed (le délai annoncé par la source, pénalisé et plafonné). |
| measured_at | Date de la dernière mesure du service par le moteur de santé ; les scores sont actualisés toutes les heures. |
Référence des endpoints
| Méthode | Endpoint | Description |
|---|---|---|
| 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. |
Schéma OpenAPI
Une définition lisible par machine de chaque endpoint. Fournissez ce fichier à un générateur de clients (openapi-generator, Kiota) ou à Postman pour obtenir un client prêt à l’emploi dans votre propre langage.
https://panelfollows.com/api/v3/openapi.jsonQuestions fréquentes
Ma clé classique fonctionne-t-elle sur la v3 ?
Oui. Vous n’avez pas besoin d’une nouvelle clé pour l’essayer. Passez tout de même à une clé v3 en production : elle peut être révoquée et n’est pas stockée en clair.
Pourquoi les prix sont-ils des chaînes ?
Les nombres à virgule flottante perdent des fractions sur les montants décimaux. Renvoyer des chaînes, que vous convertissez en type décimal de votre côté, élimine toute cette catégorie d’écarts d’arrondi.
Pourquoi le champ unit est-il important ?
La plupart des services sont facturés pour 1 000 unités (per_1000), mais les services forfaitaires sont vendus comme un article unique (per_order) dont le tarif couvre tout le forfait. Les intégrations qui ignoraient cette distinction se trompaient d’un facteur 1 000 sur le prix des forfaits.
Ma commande a été créée mais reste en attente. Que s’est-il passé ?
La transmission au fournisseur a peut-être été retardée. Le montant reste réservé sur votre solde et la commande n’est pas perdue : notre équipe la renvoie automatiquement. Le champ processing_delayed signale cet état.
Tous les services prennent-ils en charge l’annulation et le remplacement ?
Non. Vérifiez features.cancel et features.refill sur l’objet service. Appeler l’endpoint sur un service qui ne le prend pas en charge renvoie 400.
Puis-je utiliser les deux API en même temps ?
Oui. Même compte, même solde, mêmes commandes. Une commande passée via la v2 peut être lue via la v3.
L’API revendeur classique, standard dans tout le secteur. C’est le format qu’attendent les logiciels de panel prêts à l’emploi.
Endpoint
POST https://panelfollows.com/api/v2
POST https://panelfollows.com/api/v2/trAuthentification
Chaque requête contient un paramètre key. Gardez votre clé secrète et régénérez-la immédiatement en cas de fuite.
Format des requêtes et des réponses
Les requêtes sont envoyées en POST sous forme de formulaire (application/x-www-form-urlencoded) et les réponses sont en JSON. Les échecs renvoient eux aussi HTTP 200, avec { "error": "..." } dans le corps.
Également pris en charge, bien que jamais documenté jusqu’ici : vous pouvez l’appeler en GET et envoyer le corps en application/json.
Limites de requêtes
240 requêtes par minute et par clé, plus 300 par minute et par IP. Les requêtes excédentaires sont rejetées avec un code 429.
Actions et paramètres
| action | Paramètre | Description |
|---|---|---|
| services | key, action | Liste tous les services actifs (id, nom, catégorie, tarif, min/max, remplacement, annulation, drip-feed). |
| add | key, action, service, link, quantity[, runs, interval, comments, username, posts, min, max] | Crée une commande. service est l’id du service dans le catalogue. Ajoutez runs et interval pour la livraison progressive (drip-feed), ainsi que les champs correspondants pour les types spéciaux. |
| status | key, action, order | orders | Statut de commande. Utilisez order pour une seule commande, ou une liste orders séparée par des virgules pour plusieurs. |
| balance | key, action | Solde du compte et devise. |
| refill | key, action, order | orders | Crée une demande de remplacement, transmise directement au fournisseur. |
| refill_status | key, action, refill | refills | Consulte le statut d’un remplacement. |
| cancel | key, action, orders | Annule des commandes. Fonctionne uniquement pour les services dont le fournisseur prend en charge l’annulation. |
Exemple
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 }Réponses en turc
Ajoutez /tr à l’URL pour recevoir les noms de services, les catégories, les statuts de commande et les messages d’erreur en turc. Les paramètres, les actions et la structure des réponses sont identiques, et votre clé fonctionne sur les deux URL. Les champs techniques (type, refill_status, currency) restent en anglais pour garantir la compatibilité avec le standard.
Passer à la v3
La migration est facultative. Si vous franchissez le pas, l’essentiel de votre logique métier reste valable, car les noms de paramètres sont inchangés. Ce qui change, c’est le transport et la façon de lire les erreurs.
- 1Déplacez la clé du champ key du corps vers l’en-tête Authorization: Bearer.
- 2Appelez un chemin de ressource au lieu de action=... (POST /orders au lieu de add).
- 3Détectez les échecs avec le statut HTTP et error.code, plutôt qu’en vérifiant la présence d’un champ error.
- 4Comparez le statut de commande à la valeur machine, pas au texte affiché.
- 5Ajoutez un en-tête Idempotency-Key lorsque vous créez des commandes.
- 6Remplacez l’interrogation du statut (polling) par des webhooks.
Une clé donne un accès complet à votre compte. Ne la partagez pas, ne l’intégrez pas dans du code côté client et ne la publiez jamais dans un dépôt public.
Clés v3
Créez autant de clés que nécessaire, donnez un libellé à chacune et révoquez-les individuellement. De notre côté, seule une empreinte cryptographique de la clé est conservée.
Créer un compte gratuitClé classique
La clé unique utilisée par l’API revendeur classique (v2). Elle fonctionne aussi sur la v3. La régénérer invalide immédiatement l’ancienne valeur.
Sécurité
- Conservez la clé dans une variable d’environnement, jamais dans le code source.
- Ne placez jamais de clé dans du code exécuté par un navigateur : faites passer les appels par votre propre serveur.
- Créez une clé distincte par système, pour que la révocation de l’une n’affecte pas les autres.
- En cas de soupçon de fuite, déployez d’abord la nouvelle clé, puis révoquez l’ancienne.
Lorsqu’une commande change de statut, nous envoyons une notification signée à votre serveur. Vous n’avez donc jamais à interroger le statut.
Pourquoi des webhooks ?
L’interrogation régulière (polling) est à la fois lente et coûteuse : demander le statut de milliers de commandes chaque minute consomme votre limite de requêtes, et vous découvrez quand même les changements avec plusieurs minutes de retard. Avec les webhooks, le changement vous parvient au moment où il se produit.
Mise en place
- 1Préparez une URL https publique (les adresses locales et de réseau privé sont refusées).
- 2Ajoutez l’URL ci-dessous et conservez le secret de signature, affiché une seule fois.
- 3Vérifiez la signature de votre côté et répondez avec un code 2xx.
- 4Utilisez le bouton de test pour valider toute la chaîne de bout en bout.
Ce que nous envoyons
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"
}
}
}Vérifier la signature
Chaque requête contient un en-tête Webhook-Signature, où t est l’horodatage et v1 la signature. La signature est le HMAC-SHA256 de la chaîne "<timestamp>.<raw body>", calculé avec votre secret.
- 1Extrayez t et v1 de l’en-tête.
- 2Vérifiez que t ne date pas de plus de 5 minutes, pour bloquer les rejeux.
- 3Calculez le HMAC-SHA256 de "<t>.<raw body>" avec votre secret.
- 4Comparez-le à v1 en temps constant et rejetez la requête s’ils ne correspondent pas.
Exemple de vérification
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
}
});Nouvelles tentatives
La première tentative a lieu dès que l’événement se produit. Si la réponse n’est pas 2xx ou si la connexion échoue, l’envoi est relancé après 1 minute, 5 minutes, 30 minutes, 2 heures et 6 heures. Au bout de 6 tentatives, l’envoi est marqué comme échoué et apparaît dans le journal des envois.
Types d’événements
Abonnez-vous aux types qui vous intéressent, ou recevez-les tous. Un changement de statut produit exactement un événement, du type qui correspond le mieux au nouveau statut.
| order.created | Une commande a été créée. |
| order.processing | Le fournisseur a commencé à traiter la commande. |
| order.completed | La commande est terminée. |
| order.partial | La commande a été livrée partiellement et le reste a été remboursé. |
| order.canceled | La commande a été annulée ou remboursée. |
| order.updated | Le statut a changé d’une autre manière. |
| refill.created | Un remplacement a été demandé. |
| refill.updated | Le statut d’un remplacement a changé. |
| account.low_balance | Votre solde est passé sous le seuil défini avec PATCH /account. L’événement se déclenche au franchissement du seuil, pas à chaque commande, et se réarme dès que le solde repasse au-dessus. |
Si vous ne pouvez pas héberger de webhook
Les mêmes événements peuvent être lus avec un curseur depuis GET /api/v3/events. Utilisez-le pendant le développement en local, si vous n’avez pas d’IP fixe ou si vous êtes derrière un pare-feu.
// 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);Erreurs (48)
| Code | Statut | Description |
|---|---|---|
| missing_api_key | 401 | Aucune clé API n’a été fournie. Envoyez-la sous la forme « Authorization: Bearer <key> ». |
| invalid_api_key | 401 | La clé API fournie n’est pas valide. |
| revoked_api_key | 401 | Cette clé API a été révoquée et ne peut plus être utilisée. |
| account_banned | 403 | Ce compte est banni. |
| account_suspended | 403 | Ce compte est suspendu. |
| insufficient_scope | 403 | Cette clé API n’a pas l’autorisation d’accéder à cet endpoint. |
| invalid_json | 400 | Le corps de la requête n’est pas un JSON valide. |
| unsupported_content_type | 415 | Content-Type non pris en charge. Utilisez application/json ou application/x-www-form-urlencoded. |
| method_not_allowed | 405 | Cette méthode HTTP n’est pas autorisée sur cet endpoint. |
| payload_too_large | 413 | Le corps de la requête est trop volumineux. |
| missing_parameter | 400 | Un paramètre obligatoire est manquant. |
| invalid_parameter | 400 | Un paramètre a une valeur non valide. |
| invalid_link | 400 | Le lien est manquant ou n’est pas une URL http(s) valide. |
| invalid_quantity | 400 | La quantité n’est pas un nombre entier positif valide. |
| quantity_out_of_range | 400 | La quantité est en dehors de la plage autorisée pour ce service. |
| invalid_comments | 400 | Le champ des commentaires est vide ou contient trop de lignes. |
| invalid_username | 400 | Le nom d’utilisateur n’est pas valide pour ce service. |
| invalid_subscription | 400 | Les paramètres de l’abonnement ne sont pas valides. |
| invalid_runs | 400 | La valeur « runs » n’est pas valide pour la livraison progressive (drip-feed). |
| invalid_interval | 400 | La valeur « interval » n’est pas valide pour la livraison progressive (drip-feed). |
| dripfeed_not_supported | 400 | Ce service ne prend pas en charge la livraison progressive (drip-feed). |
| missing_required_field | 400 | Un champ requis par ce type de service est manquant ou non valide. |
| service_inactive | 400 | Ce service n’est pas disponible à la commande pour le moment. |
| invalid_cursor | 400 | Le curseur de pagination n’est pas valide. |
| invalid_limit | 400 | Le paramètre « limit » est en dehors de la plage autorisée. |
| invalid_webhook_url | 400 | L’URL du webhook doit être une adresse https:// publique. |
| invalid_events | 400 | Un ou plusieurs types d’événements demandés sont inconnus. |
| batch_too_large | 400 | Trop d’éléments dans une seule requête groupée. |
| cancel_not_supported | 400 | Ce service ne prend pas en charge l’annulation. |
| refill_not_supported | 400 | Ce service ne propose pas de remplacement. |
| unknown_endpoint | 404 | Endpoint inconnu. Consultez la référence de l’API pour connaître les routes disponibles. |
| service_not_found | 404 | Aucun service ne correspond à cet id. |
| order_not_found | 404 | Aucune commande ne correspond à cet id sur votre compte. |
| refill_not_found | 404 | Aucun remplacement ne correspond à cet id sur votre compte. |
| webhook_not_found | 404 | Aucun endpoint webhook ne correspond à cet id sur votre compte. |
| order_not_cancelable | 409 | Cette commande ne peut plus être annulée en raison de son statut actuel. |
| cancel_rejected | 409 | Le fournisseur a refusé la demande d’annulation. |
| order_not_completed | 409 | Un remplacement ne peut être demandé que pour une commande terminée. |
| duplicate_link | 409 | Une commande est déjà active pour ce lien. Attendez qu’elle soit terminée. |
| idempotency_key_reuse | 409 | Cette clé Idempotency-Key a déjà été utilisée avec un corps de requête différent. |
| idempotency_in_progress | 409 | Une requête portant cette clé Idempotency-Key est encore en cours de traitement. Réessayez dans quelques instants. |
| webhook_limit_reached | 409 | Vous avez atteint le nombre maximal d’endpoints webhook. |
| insufficient_balance | 402 | Solde insuffisant pour cette commande. |
| rate_limit_exceeded | 429 | Limite de requêtes dépassée. Consultez l’en-tête de réponse Retry-After. |
| provider_error | 502 | Le fournisseur en amont a renvoyé une erreur. Réessayez. |
| refill_failed | 502 | La demande de remplacement a été refusée par le fournisseur. |
| service_temporarily_unavailable | 503 | Ce service est temporairement indisponible. Réessayez plus tard. |
| internal_error | 500 | Une erreur inattendue s’est produite de notre côté. |