GoPhish API : automatiser vos campagnes avec curl et Python
Clé API, endpoints des groupes, modèles, pages, profils SMTP et campagnes : exemples curl et Python vérifiés sur GoPhish 0.12.1, et les pièges de l'API.
L’interface web de GoPhish est une couche posée sur une API JSON. La documentation officielle le dit sans détour : le projet a été conçu « API-first ». Tout ce que vous faites à la souris (créer un groupe, préparer un modèle, lancer une campagne, lire les résultats) passe par des requêtes HTTP que vous pouvez scripter. Ça devient utile dès que la liste des cibles vient d’un export RH, qu’une campagne doit partir à une date que l’administrateur ne connaît pas lui-même, ou que les résultats doivent finir dans un tableur.
Les exemples de ce guide ont été vérifiés sur la documentation de l’API et sur le code source de la version 0.12.1, la dernière publiée (14 septembre 2022, github.com/gophish/gophish/releases). Ils supposent une instance installée comme dans notre tutoriel GoPhish sur VPS ou notre guide Docker Compose, et un cadre posé : mandat écrit de la direction, salariés informés qu’un programme de simulation existe.
La clé API et l’adresse de l’API
La clé se trouve dans l’interface d’administration, page Settings (/settings), champ « API Key ». Le bouton « Reset » en génère une nouvelle. Chaque utilisateur GoPhish a sa clé, avec les droits de son rôle : admin gère aussi les utilisateurs et les webhooks, user se limite aux campagnes et à leurs objets.
GoPhish accepte la clé de deux façons, d’après le middleware RequireAPIKey du code source :
- dans l’en-tête
Authorization, avec ou sans le préfixeBearer(il est retiré s’il est présent) ; - dans le paramètre
api_keyde l’URL. À éviter : la clé finit dans l’historique du navigateur et dans les journaux de votre reverse proxy.
L’API est servie par le serveur d’administration, sur 127.0.0.1:3333 par défaut. Le config.json livré active le TLS avec un certificat auto-signé créé au premier démarrage, sans nom d’hôte. Depuis le serveur ou à travers un tunnel SSH, désactivez donc la vérification du certificat (curl -k, verify=False en Python). Derrière votre reverse proxy avec un certificat Let’s Encrypt, gardez-la active.
# Depuis votre poste : tunnel vers l'administration, rien d'exposé sur Internet
ssh -L 3333:127.0.0.1:3333 admin@votre-serveur
# Dans un autre terminal
export GOPHISH_URL="https://127.0.0.1:3333"
export GOPHISH_API_KEY="collez-ici-la-cle-de-la-page-settings"
curl -sk -H "Authorization: Bearer $GOPHISH_API_KEY" \
"$GOPHISH_URL/api/campaigns/" | jq length
Une instance neuve répond 0. Si la clé est fausse, vous obtenez un code 401 et {"message": "Invalid API Key", "success": false, "data": null} ; si l’en-tête manque, le message devient API Key not set.
En Docker, la variable d’environnement GOPHISH_INITIAL_ADMIN_API_TOKEN fixe la clé du compte admin au moment où GoPhish crée sa base. Elle n’agit qu’au premier démarrage, mais elle évite d’aller chercher la clé à la main dans un déploiement scripté.
Le piège du slash final
GoPhish déclare ses routes avec l’option StrictSlash du routeur gorilla/mux. Les collections finissent par une barre oblique : /api/groups/, /api/templates/, /api/pages/, /api/smtp/, /api/campaigns/. Appelez /api/templates sans la barre et le serveur répond par une redirection 301.
Selon le client, l’effet change. curl s’arrête sur la redirection : rien n’est créé et la réponse est vide (avec curl -i, vous voyez le code 301 et l’en-tête Location). La bibliothèque Python requests suit la redirection en transformant le POST en GET : elle reçoit la liste des modèles existants avec un code 200, et votre script croit avoir réussi. La documentation de gorilla/mux décrit ce comportement des clients pour les méthodes POST et PUT.
La règle à retenir : barre finale sur les collections, rien ailleurs (/api/groups/7, /api/campaigns/12/results, /api/import/group, /api/util/send_test_email). Dans vos scripts Python, passez allow_redirects=False pour qu’une erreur de chemin se voie tout de suite.
Les endpoints utiles
| Objet | Collection (GET, POST) | Élément (GET, PUT, DELETE) | À savoir |
|---|---|---|---|
| Groupes de cibles | /api/groups/ | /api/groups/:id | /api/groups/summary donne les effectifs sans la liste |
| Import CSV | POST /api/import/group | renvoie les cibles en JSON, n’enregistre rien | |
| Modèles d’email | /api/templates/ | /api/templates/:id | variables sensibles à la casse |
| Landing pages | /api/pages/ | /api/pages/:id | capture_passwords à laisser à false |
| Profils d’envoi | /api/smtp/ | /api/smtp/:id | le GET renvoie les mots de passe SMTP en clair |
| Campagnes | /api/campaigns/ | /api/campaigns/:id | sous-routes /results, /summary, /complete |
| Test d’envoi | POST /api/util/send_test_email | synchrone, renvoie l’erreur du serveur SMTP | |
| Utilisateurs, webhooks | /api/users/, /api/webhooks/ | rôle admin requis |
Les erreurs suivent toutes le même format : {"message": "...", "success": false, "data": null}, avec les codes 400, 404, 409 (nom déjà pris) ou 500.
Pour les exemples Python, une petite fonction suffit. Elle utilise requests et refuse les redirections :
import os
import requests
URL = os.environ["GOPHISH_URL"].rstrip("/")
VERIFY_TLS = os.environ.get("GOPHISH_VERIFY_TLS", "1") != "0" # "0" via un tunnel SSH
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['GOPHISH_API_KEY']}"
session.verify = VERIFY_TLS
def api(method, path, **kwargs):
"""Appelle l'API GoPhish et lève une erreur lisible si la réponse n'est pas 2xx."""
r = session.request(method, URL + path, allow_redirects=False,
timeout=30, **kwargs)
if not 200 <= r.status_code < 300:
raise RuntimeError(f"{method} {path} : HTTP {r.status_code} {r.text[:300]}")
return r.json()
Avec GOPHISH_VERIFY_TLS=0, urllib3 affiche un avertissement à chaque requête ; urllib3.disable_warnings() le fait taire si vous passez par le tunnel.
Créer et tenir à jour un groupe de cibles
Un groupe porte un nom unique et une liste de cibles. Chaque cible a quatre champs : email, first_name, last_name et position. GoPhish n’a pas de champ « service » : la colonne position sert souvent à ça.
curl -sk -X POST "$GOPHISH_URL/api/groups/" \
-H "Authorization: Bearer $GOPHISH_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Comptabilité",
"targets": [
{"email": "marie.dupont@exemple.fr", "first_name": "Marie",
"last_name": "Dupont", "position": "Comptabilité"},
{"email": "jean.martin@exemple.fr", "first_name": "Jean",
"last_name": "Martin", "position": "Comptabilité"}
]
}'
La réponse (code 201) contient l’id du groupe. Un nom déjà pris donne un code 409 et le message Group name already in use.
Importer un fichier CSV
L’endpoint /api/import/group lit un CSV envoyé en multipart et renvoie les cibles en JSON, sans rien enregistrer. GoPhish repère les colonnes par leur nom, sans tenir compte de la casse : First Name, Last Name, Email, Position (le code accepte aussi first_name ou first-name). On enchaîne donc deux appels :
curl -sk -H "Authorization: Bearer $GOPHISH_API_KEY" \
-F "file=@cibles.csv" "$GOPHISH_URL/api/import/group" \
| jq '{name: "Comptabilité", targets: .}' \
| curl -sk -X POST "$GOPHISH_URL/api/groups/" \
-H "Authorization: Bearer $GOPHISH_API_KEY" \
-H "Content-Type: application/json" --data @-
Synchroniser un groupe depuis un export RH
Le PUT /api/groups/:id remplace la liste des membres par celle que vous envoyez. Le code de la version 0.12.1 retire les adresses absentes, met à jour le nom et la colonne position des adresses connues, et ajoute les nouvelles. Le corps doit contenir le même id que l’URL, sinon l’API répond Error: /:id and group_id mismatch.
def synchroniser_groupe(nom, cibles):
"""Crée le groupe s'il n'existe pas, sinon remplace ses membres par `cibles`."""
resume = api("GET", "/api/groups/summary") # 0.12.1 : {"total": n, "groups": [...]}
existants = {g["name"]: g["id"] for g in resume["groups"]}
if nom in existants:
gid = existants[nom]
corps = {"id": gid, "name": nom, "targets": cibles}
return api("PUT", f"/api/groups/{gid}", json=corps)
return api("POST", "/api/groups/", json={"name": nom, "targets": cibles})
La documentation montre une liste pour /api/groups/summary, mais le code de la 0.12.1 renvoie un objet avec les champs total et groups : le script suit le code. Lancé chaque nuit sur l’export du SIRH, il tient les groupes à jour sans passer par l’interface. Les départs disparaissent des groupes ; les campagnes passées gardent leurs résultats.
Créer un modèle et une landing page
L’API crée le modèle, pas son contenu. Pour écrire le texte en français, le kit de conception de campagnes GoPhish donne 18 thèmes avec leurs signaux et leur débrief.
Un modèle contient un name, un subject, une version html et une version text (au moins l’une des deux), des attachments facultatives encodées en base64 et un envelope_sender facultatif. Malgré son nom, ce dernier fixe l’en-tête From affiché au destinataire ; vide, GoPhish reprend l’adresse SMTP From du profil d’envoi. Notre guide du profil d’envoi SMTP détaille la différence entre les deux.
Les variables disponibles sont {{.FirstName}}, {{.LastName}}, {{.Email}}, {{.Position}}, {{.From}}, {{.URL}}, {{.Tracker}}, {{.TrackingURL}}, {{.RId}} et {{.BaseURL}}. Elles sont sensibles à la casse, et les modèles passent par le moteur text/template de Go : une condition comme {{if .FirstName}}...{{end}} fonctionne, GoPhish s’en sert lui-même dans son email de test par défaut.
Le plus simple est d’écrire le HTML dans un fichier et de laisser jq (version 1.6 ou plus récente) construire le JSON, ce qui évite d’échapper les guillemets :
cat > modele-test.html <<'EOF'
<html><body>
<p>Bonjour {{.FirstName}},</p>
<p>Ce message vérifie la configuration de la campagne.</p>
<p><a href="{{.URL}}">Lien de test</a></p>
{{.Tracker}}</body></html>
EOF
jq -n --rawfile html modele-test.html \
'{name: "Test - vérification de la chaîne", subject: "Message de test", html: $html}' \
| curl -sk -X POST "$GOPHISH_URL/api/templates/" \
-H "Authorization: Bearer $GOPHISH_API_KEY" \
-H "Content-Type: application/json" --data @-
Une variable mal orthographiée ({{.Prenom}}, {{.firstname}}) fait échouer la validation du modèle. L’API répond alors un code 500 avec le seul message Error inserting template into database : relisez les variables avant de chercher ailleurs.
En Python, même principe :
from pathlib import Path
api("POST", "/api/templates/", json={
"name": "Test - vérification de la chaîne (fichier)",
"subject": "Message de test",
"html": Path("modele.html").read_text(encoding="utf-8"),
})
La landing page suit le même principe. Pour un programme de sensibilisation, servez une page de débrief et laissez la capture désactivée :
debrief = """<html><body>
<h1>Ceci était un exercice</h1>
<p>Le message que vous venez d'ouvrir faisait partie d'un exercice interne.
Prenez une minute pour relire l'expéditeur et l'adresse du lien.</p>
</body></html>"""
api("POST", "/api/pages/", json={
"name": "Débrief - exercice de sensibilisation",
"html": debrief,
"capture_credentials": False,
"capture_passwords": False,
"redirect_url": "",
})
Avec capture_passwords à true, les mots de passe saisis sont enregistrés tels quels dans la base de GoPhish. Personne n’a besoin de ces données pour mesurer un taux de clic, et leur présence complique votre registre RGPD.
Créer un profil d’envoi et le tester
curl -sk -X POST "$GOPHISH_URL/api/smtp/" \
-H "Authorization: Bearer $GOPHISH_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Relais - simulations",
"interface_type": "SMTP",
"from_address": "Service informatique <it@simulation.exemple.fr>",
"host": "smtp.relais.exemple:587",
"username": "identifiant-smtp",
"password": "mot-de-passe-smtp",
"ignore_cert_errors": false,
"headers": []
}'
Sans port dans host, GoPhish utilise le port 25. Avant d’écrire une campagne, testez le profil : POST /api/util/send_test_email envoie un message tout de suite et renvoie l’erreur exacte du serveur SMTP. Sans modèle précisé, GoPhish envoie son message par défaut, dont l’objet est « Default Email from Gophish ».
curl -sk -X POST "$GOPHISH_URL/api/util/send_test_email" \
-H "Authorization: Bearer $GOPHISH_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"smtp": {"name": "Relais - simulations"},
"email": "vous@exemple.fr",
"first_name": "Test"
}'
La réponse vaut {"success": true, "message": "Email Sent"} ou contient le message d’erreur du serveur. Le guide du profil d’envoi SMTP explique comment lire ces erreurs avec Microsoft 365, Google Workspace et OVH.
Lancer une campagne planifiée
Une campagne désigne le modèle, la page, le profil et les groupes par leur nom. Chacun doit donc exister avant l’appel, sinon l’API répond Template not found, Page not found, Sending profile not found ou Group not found.
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
debut = datetime(2026, 11, 17, 9, 30, tzinfo=ZoneInfo("Europe/Paris"))
fin = debut + timedelta(hours=3)
campagne = api("POST", "/api/campaigns/", json={
"name": "Exercice novembre - Comptabilité",
"template": {"name": "Test - vérification de la chaîne"},
"page": {"name": "Débrief - exercice de sensibilisation"},
"smtp": {"name": "Relais - simulations"},
"url": "https://simulation.exemple.fr",
"launch_date": debut.isoformat(), # 2026-11-17T09:30:00+01:00
"send_by_date": fin.isoformat(), # envois étalés jusqu'à 12h30
"groups": [{"name": "Comptabilité"}],
})
print(campagne["id"], campagne["status"])
Le champ url est l’adresse publique de votre serveur de phishing (le phish_server), celle que GoPhish place dans {{.URL}}. Une launch_date passée ou absente lance la campagne immédiatement (statut In progress) ; une date future la met en file (Queued). GoPhish stocke les dates en UTC et exige le format RFC 3339 : une date sans décalage horaire est refusée (Invalid JSON structure), et une date en Z est lue en UTC, donc 09:30:00Z part à 10h30 heure de Paris en hiver et à 11h30 en été. Construisez la date avec son fuseau, comme ci-dessus.
Le send_by_date étale les envois entre les deux dates, ce qui ménage le serveur SMTP et évite qu’un service entier reçoive le message à la même minute. Il doit être postérieur au lancement, sinon l’API refuse la campagne.
Lire les résultats
Le résumé donne les compteurs :
curl -sk -H "Authorization: Bearer $GOPHISH_API_KEY" \
"$GOPHISH_URL/api/campaigns/12/summary" | jq .stats
{
"total": 42, "sent": 42, "opened": 25, "clicked": 9,
"submitted_data": 0, "email_reported": 6, "error": 0
}
Le détail, GET /api/campaigns/:id/results, renvoie une ligne par personne (email, first_name, last_name, position, status, reported, send_date, ip) et la chronologie des événements (timeline). Le statut garde l’étape la plus avancée : une personne qui a cliqué reste à Clicked Link même si elle rouvre le message ensuite. Le signalement est un champ à part, le booléen reported.
import csv
from collections import Counter
res = api("GET", f"/api/campaigns/{campagne['id']}/results")
with open("resultats.csv", "w", newline="", encoding="utf-8") as f:
w = csv.writer(f, delimiter=";")
w.writerow(["email", "service", "statut", "signalé"])
for r in res["results"]:
signale = "oui" if r["reported"] else "non"
w.writerow([r["email"], r["position"], r["status"], signale])
cliques = Counter(r["position"] for r in res["results"]
if r["status"] in ("Clicked Link", "Submitted Data"))
print(cliques.most_common())
Prudence en lisant ces chiffres. L’ouverture dépend de l’image de suivi ({{.Tracker}}) : un client de messagerie qui bloque les images n’en déclenche pas, donc le taux d’ouverture est sous-estimé. Et une ligne au statut Error ou Retrying relève d’un problème d’envoi, à corriger avant de compter les clics.
Pour arrêter une campagne, appelez GET /api/campaigns/:id/complete (un GET, oui) : les envois en attente sont annulés et les clics suivants tombent sur une page 404. DELETE /api/campaigns/:id supprime la campagne avec ses résultats et sa chronologie. Exportez avant de supprimer.
Recevoir les événements en temps réel
Les administrateurs déclarent des webhooks dans la page « Webhooks ». GoPhish envoie alors chaque événement de campagne (envoi, ouverture, clic, signalement) en POST vers votre URL, au format JSON : campaign_id, email, time, message, details. Chaque requête porte l’en-tête X-Gophish-Signature: sha256=..., un HMAC-SHA256 du corps calculé avec le secret du webhook.
import hashlib
import hmac
def signature_valide(corps: bytes, entete: str, secret: str) -> bool:
mac = hmac.new(secret.encode(), corps, hashlib.sha256).hexdigest()
return hmac.compare_digest("sha256=" + mac, entete or "")
Cas d’usage typique : prévenir l’équipe sécurité dès qu’un salarié signale le message, ou alimenter un tableau de bord sans interroger l’API toutes les minutes. GoPhish abandonne l’appel au bout de 10 secondes : répondez vite et traitez l’événement ensuite.
Le client Python officiel
Le projet publie un client, le paquet gophish sur PyPI (github.com/gophish/api-client-python). Sa dernière version, la 0.5.1, date du 18 septembre 2020.
import os
from gophish import Gophish
api_gp = Gophish(os.environ["GOPHISH_API_KEY"],
host="https://127.0.0.1:3333", verify=False)
for c in api_gp.campaigns.get():
print(c.id, c.name, c.status)
resume = api_gp.campaigns.summary(campaign_id=12)
print(resume.stats.clicked)
Il reste utilisable pour lire les campagnes et créer les objets courants. Son code a cependant des manques : le modèle de template ignore envelope_sender, et le résumé de campagne ignore email_reported. Pour un script que vous allez maintenir plusieurs années, la fonction api() plus haut tient en dix lignes et couvre tous les champs.
Protéger la clé
La clé API a tous les droits de son utilisateur. Avec elle, on lit les groupes (des données personnelles), les données saisies sur les landing pages, et les mots de passe SMTP en clair via GET /api/smtp/. Traitez-la comme un mot de passe :
- rangez-la dans un coffre ou une variable d’environnement, jamais dans un dépôt Git ;
- changez-la avec le bouton « Reset » ou
POST /api/reset, qui renvoie la nouvelle clé dans le champdata; - gardez le port 3333 fermé sur Internet : tunnel SSH, VPN ou liste d’IP autorisées, comme dans la partie durcissement du tutoriel VPS ;
- créez un compte par personne et réservez le rôle admin à ceux qui gèrent les utilisateurs et les webhooks.
Ce que l’API ne fait pas à votre place
L’API automatise la mécanique de GoPhish. Le reste du programme reste à votre charge : écrire des scénarios crédibles en français, montrer à la personne qui a cliqué ce qu’elle aurait dû repérer, produire une preuve lisible pour un audit, et maintenir un serveur dont la dernière version date de septembre 2022.
Si c’est là que passe votre temps, comparez avec une plateforme hébergée. nophi.sh propose 80 scénarios en français et en anglais, assigne un module court après un test échoué, relance les campagnes selon le niveau de risque de chaque personne, et produit un rapport d’audit PDF avec export CSV, avec des données hébergées en France. Notre comparatif GoPhish chiffre les deux options, et le guide migrer depuis GoPhish montre comment reprendre vos cibles et vos modèles.
FAQ
Où trouver la clé API de GoPhish ?
Dans l’interface d’administration, page Settings (adresse /settings), champ « API Key ». Le bouton « Reset » en génère une nouvelle et invalide l’ancienne ; l’endpoint POST /api/reset fait la même chose. Chaque utilisateur GoPhish a sa propre clé, avec les droits de son rôle (admin ou user). La clé s’envoie dans l’en-tête Authorization, avec ou sans le préfixe Bearer.
Pourquoi mon POST renvoie-t-il une liste au lieu de créer l’objet ?
Il manque la barre oblique finale. GoPhish déclare ses collections avec un slash final (/api/templates/, /api/groups/, /api/campaigns/) et redirige les autres formes en 301. curl s’arrête sur la redirection sans rien créer. La bibliothèque Python requests la suit, mais transforme le POST en GET : vous recevez la liste existante avec un code 200. Ajoutez le slash final sur les collections, et passez allow_redirects=False dans vos scripts pour que l’erreur se voie.
Peut-on importer un fichier CSV de cibles par l’API ?
Oui, en deux temps. POST /api/import/group reçoit le fichier en multipart et renvoie les cibles en JSON, sans rien enregistrer. Vous envoyez ensuite ce JSON dans le champ targets d’un POST /api/groups/. GoPhish reconnaît les colonnes First Name, Last Name, Email et Position, sans tenir compte de la casse.
Comment planifier une campagne à une date précise ?
Renseignez launch_date au format ISO 8601 avec le fuseau, par exemple 2026-11-17T09:30:00+01:00. Une date passée ou absente lance la campagne tout de suite. Pour étaler les envois, ajoutez send_by_date : GoPhish répartit les emails entre les deux dates, qui doivent être dans cet ordre.
Le client Python officiel de GoPhish est-il encore maintenu ?
Le paquet gophish sur PyPI en est à la version 0.5.1, publiée le 18 septembre 2020. Il fonctionne pour lister et créer les objets courants, mais son modèle de template ignore le champ envelope_sender et son résumé de campagne ignore le compteur email_reported. Pour un script que vous allez maintenir, la bibliothèque requests et quelques lignes suffisent et couvrent tous les champs.
Comment supprimer les données d’une campagne terminée ?
DELETE /api/campaigns/:id supprime la campagne avec ses résultats, sa chronologie d’événements et les envois en attente. Exportez d’abord ce que vous devez garder comme preuve (GET /api/campaigns/:id/results), puis supprimez selon la durée de conservation prévue dans votre registre RGPD.