Clusters

Un cluster est une liste de sites enregistrée. Via l’API, vous pouvez en créer un à partir d’une recherche ou de votre propre liste, combiner des clusters et extraire des valeurs du code source de chaque site d’un cluster - contacts, profils sur les réseaux sociaux, identifiants d’analytics et de balises, tout ce qu’une expression régulière peut capturer.

Les points de terminaison

Point de terminaisonMéthodeRôle
/v1/clustersGETLes clusters du compte, du plus récent au plus ancien : identifiant, nom, nombre de domaines, date de création.
/v1/clustersPOSTCrée un cluster à partir d’une recherche (query) ou d’une liste (domains) ; name est facultatif.
/v1/clusters/{id}GETLe cluster et une page de ses domaines : page, per_page (jusqu’à 10 000), format=txt pour un domaine par ligne.
/v1/clusters/combinePOSTUn nouveau cluster à partir de clusters existants : operation and, or ou diff, clusters - une liste d’identifiants.
/v1/clusters/{id}/extractGET, POSTExtrait des valeurs avec presets et regex, partie par partie : offset, limit (jusqu’à 1 000 sites par appel), format json, xml ou csv.
/v1/clusters/presetsGETLes expressions toutes faites, avec leurs motifs.
/v1/clusters/{id}/renamePOSTNouveau name.
/v1/clusters/{id}/deletePOSTSupprime définitivement le cluster.

Tout ce qui modifie un cluster passe par un POST avec un corps JSON - ni PUT ni DELETE : tout client capable de lancer une recherche peut donc aussi gérer des clusters. Le jeton, la limite de débit et le format des erreurs sont les mêmes que pour la recherche ; les chiffres des clusters dans la réponse de /v1/account indiquent combien vous en avez et combien de points d’extraction il vous reste aujourd’hui.

Créer un cluster

À partir d’une recherche : les résultats de la requête, dans la limite du nombre de lignes de votre forfait, deviennent le cluster. Cela coûte une recherche, comme /v1/search.

curl https://api.publicwww.com/v1/clusters \
     -H "Authorization: Bearer $PUBLICWWW_KEY" \
     -H "Content-Type: application/json" \
     -d '{"query": "\"googletagmanager.com/gtm.js\"", "name": "GTM sites"}'

{"id": 7, "name": "GTM sites", "size": 100000, "created": "2026-10-10T20:21:30Z",
 "query": "\"googletagmanager.com/gtm.js\"", "total": 2412577, "index_complete": true}

À partir d’une liste : des domaines ou des URL, sous forme de tableau JSON ou un par ligne. Seuls les sites présents dans l’index sont conservés - submitted indique combien vous en avez envoyé, size combien ont été retenus. Une liste ne coûte rien.

curl https://api.publicwww.com/v1/clusters \
     -H "Authorization: Bearer $PUBLICWWW_KEY" \
     -H "Content-Type: application/json" \
     -d '{"domains": ["example.com", "https://www.example.org/about"], "name": "Prospects"}'

Le forfait plafonne le nombre de domaines d’un cluster, et un compte conserve jusqu’à 100 clusters. S’il y en a déjà 100, une nouvelle création renvoie 409 cluster_limit - rien n’est supprimé à votre place ; supprimez d’abord ceux dont vous n’avez plus besoin.

Combiner des clusters

curl https://api.publicwww.com/v1/clusters/combine \
     -H "Authorization: Bearer $PUBLICWWW_KEY" \
     -H "Content-Type: application/json" \
     -d '{"operation": "diff", "clusters": [7, 3], "name": "GTM, not yet contacted"}'

and garde les domaines présents dans tous les clusters, or ceux présents dans au moins l’un d’eux, et diff ceux du premier cluster qui ne sont pas dans le second - exactement deux clusters dans ce cas. La combinaison ne coûte rien.

Extraire des données

L’extraction lit les pages indexées de chaque site du cluster et renvoie ce que les expressions capturent, une colonne par expression. Le plus simple est d’utiliser un modèle prédéfini :

Modèle prédéfiniCe qu’il extrait
emailAdresses tirées des liens mailto:
phoneNuméros tirés des liens tel:
whatsapp, telegram, skypeNuméros WhatsApp, noms d’utilisateur Telegram et identifiants Skype tirés de leurs liens
facebook, instagram, twitter, linkedinLiens vers les profils du site sur les réseaux sociaux (twitter reconnaît aussi x.com, linkedin les pages entreprise)
gtm, ga4, uaIdentifiants de conteneur Google Tag Manager, Google Analytics 4 et Universal Analytics
hotjarIdentifiant de site Hotjar
adsenseIdentifiant d’éditeur AdSense
bitcoinAdresses tirées des liens de paiement bitcoin:

Ou écrivez la vôtre : une expression régulière entre barres obliques (ou barres verticales) avec les options facultatives i, m, s, u, de 200 caractères au plus. Le premier groupe capturant est la valeur - la même règle que pour snipexp:. Jusqu’à dix expressions par appel, modèles prédéfinis compris.

curl https://api.publicwww.com/v1/clusters/7/extract \
     -H "Authorization: Bearer $PUBLICWWW_KEY" \
     -H "Content-Type: application/json" \
     -d '{"presets": ["gtm", "email"], "regex": ["/data-site-id=\"([0-9]+)\"/i"], "limit": 1000}'

{
  "cluster": 7, "name": "GTM sites", "size": 100000,
  "offset": 0, "scanned": 1000, "in_index": 1000, "with_matches": 941,
  "next_offset": 1000,
  "regex": ["/(GTM-[A-Z0-9]{4,10})\\b/", "/mailto:(...)/i", "/data-site-id=\"([0-9]+)\"/i"],
  "points_used": 1834.2, "points_left": 98165,
  "rows": [
    { "domain": "example.com", "values": [["GTM-AB12CD"], ["info@example.com"], []], "matches": 2 }
  ]
}

Partie par partie. Un appel parcourt jusqu’à 1 000 sites du cluster, à partir de offset. Rappelez ensuite l’API avec offset égal à next_offset, jusqu’à ce que celui-ci vaille null. Les sites sans correspondance sont omis ; skip_empty=0 les liste aussi. format=csv donne une ligne par site - le domaine, puis une colonne par expression - avec le décalage suivant dans l’en-tête X-Next-Offset.

Points. L’extraction consomme le quota d’extraction quotidien de votre forfait : chaque site trouvé dans l’index coûte autant de points que de valeurs trouvées, 0,1 s’il n’y en a aucune. Quand les points sont épuisés, l’appel s’arrête plus tôt avec "stopped": "extract_quota_exceeded" et renvoie ce qu’il a déjà ; un appel sans aucun point restant reçoit 429 extract_quota_exceeded. Essayez d’abord une expression avec un petit limit - voir les limites des clusters.

Un cluster entier, en code

Créer un cluster à partir d’une recherche et écrire dans un fichier CSV les valeurs extraites de chaque site. Les bibliothèques clientes proposent la même chose sous forme de fonctions prêtes à l’emploi et d’une ligne de commande.

Python

import csv, os, time, requests

KEY  = os.environ["PUBLICWWW_KEY"]
BASE = "https://api.publicwww.com"
H    = {"Authorization": "Bearer " + KEY}

def call(method, path, body=None):
    while True:
        r = requests.request(method, BASE + path, headers=H, json=body)
        if r.status_code == 429 and r.json()["error"]["code"] == "too_many_requests":
            time.sleep(int(r.headers.get("Retry-After", 30)))
            continue
        r.raise_for_status()
        return r.json()

cluster = call("POST", "/v1/clusters", {"query": '"googletagmanager.com/gtm.js"'})
offset = 0
with open("extract.csv", "w", newline="") as f:
    out = csv.writer(f)
    while offset is not None:
        part = call("POST", "/v1/clusters/%d/extract" % cluster["id"],
                    {"presets": ["gtm", "email"], "offset": offset})
        for row in part["rows"]:
            out.writerow([row["domain"]] + [" ".join(v) for v in row["values"]])
        offset = part["next_offset"]

JavaScript (Node 18+)

const BASE = "https://api.publicwww.com";
const H = { Authorization: "Bearer " + process.env.PUBLICWWW_KEY,
            "Content-Type": "application/json" };

async function call(method, path, body) {
  for (;;) {
    const r = await fetch(BASE + path, { method, headers: H,
                                         body: body && JSON.stringify(body) });
    const data = await r.json();
    if (r.status === 429 && data.error.code === "too_many_requests") {
      await new Promise(ok => setTimeout(ok, 1000 * (r.headers.get("Retry-After") || 30)));
      continue;
    }
    if (!r.ok) throw new Error(data.error.message);
    return data;
  }
}

const cluster = await call("POST", "/v1/clusters", { query: '"hotjar.com"' });
for (let offset = 0; offset !== null; ) {
  const part = await call("POST", `/v1/clusters/${cluster.id}/extract`,
                          { presets: ["hotjar", "email"], offset });
  for (const row of part.rows) console.log(row.domain, row.values.map(v => v.join(" ")).join(";"));
  offset = part.next_offset;
}

PHP

<?php
function call ($method, $path, $body = null) {
    $ch = curl_init ("https://api.publicwww.com" . $path);
    curl_setopt_array ($ch, [
        CURLOPT_CUSTOMREQUEST  => $method,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER     => ["Authorization: Bearer " . getenv ("PUBLICWWW_KEY"),
                                   "Content-Type: application/json"],
        CURLOPT_POSTFIELDS     => $body === null ? null : json_encode ($body),
    ]);
    $data = json_decode (curl_exec ($ch), true);
    if (isset ($data ["error"])) throw new Exception ($data ["error"]["message"]);
    return $data;
}

$cluster = call ("POST", "/v1/clusters", ["query" => '"jquery.min.js"']);
$out = fopen ("extract.csv", "w");
for ($offset = 0; $offset !== null; ) {
    $part = call ("POST", "/v1/clusters/" . $cluster ["id"] . "/extract",
                  ["presets" => ["email", "phone"], "offset" => $offset]);
    foreach ($part ["rows"] as $row)
        fputcsv ($out, array_merge ([$row ["domain"]], array_map (fn ($v) => join (" ", $v), $row ["values"])));
    $offset = $part ["next_offset"];
}

Go

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"strings"
)

func call(method, path string, body, out any) error {
	b, _ := json.Marshal(body)
	req, _ := http.NewRequest(method, "https://api.publicwww.com"+path, bytes.NewReader(b))
	req.Header.Set("Authorization", "Bearer "+os.Getenv("PUBLICWWW_KEY"))
	req.Header.Set("Content-Type", "application/json")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode >= 300 {
		return fmt.Errorf("publicwww: %s", resp.Status)
	}
	return json.NewDecoder(resp.Body).Decode(out)
}

func main() {
	var cluster struct{ ID int `json:"id"` }
	if err := call("POST", "/v1/clusters", map[string]any{"query": `"googletagmanager.com/gtm.js"`}, &cluster); err != nil {
		panic(err)
	}
	for offset := 0; ; {
		var part struct {
			Rows []struct {
				Domain string     `json:"domain"`
				Values [][]string `json:"values"`
			} `json:"rows"`
			NextOffset *int `json:"next_offset"`
		}
		path := fmt.Sprintf("/v1/clusters/%d/extract", cluster.ID)
		if err := call("POST", path, map[string]any{"presets": []string{"gtm", "ga4"}, "offset": offset}, &part); err != nil {
			panic(err)
		}
		for _, r := range part.Rows {
			cells := []string{r.Domain}
			for _, v := range r.Values {
				cells = append(cells, strings.Join(v, " "))
			}
			fmt.Println(strings.Join(cells, ";"))
		}
		if part.NextOffset == nil {
			break
		}
		offset = *part.NextOffset
	}
}

Ruby

require "json"
require "net/http"

def call(path, body)
  uri = URI("https://api.publicwww.com" + path)
  req = Net::HTTP::Post.new(uri, "Authorization" => "Bearer #{ENV.fetch('PUBLICWWW_KEY')}",
                                  "Content-Type" => "application/json")
  req.body = body.to_json
  res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
  data = JSON.parse(res.body)
  raise data["error"]["message"] if data["error"]
  data
end

cluster = call("/v1/clusters", { query: '"hotjar.com"' })
offset = 0
while offset
  part = call("/v1/clusters/#{cluster['id']}/extract", { presets: %w[hotjar email], offset: offset })
  part["rows"].each { |r| puts [r["domain"], *r["values"].map { |v| v.join(" ") }].join(";") }
  offset = part["next_offset"]
end

Erreurs

Statut et codeSignification
404 cluster_not_foundAucun cluster avec cet identifiant sur ce compte.
409 cluster_limitLe compte conserve déjà 100 clusters.
400 invalid_regexUne expression n’est pas une expression PCRE entre barres obliques ou barres verticales, de 200 caractères au plus.
400 unknown_presetModèle prédéfini inconnu ; la réponse donne la liste des modèles existants.
400 missing_source, ambiguous_sourceLa création exige soit query, soit domains.
429 extract_quota_exceededLes points d’extraction du jour sont épuisés.

La liste complète figure sur la page des erreurs et dans la description de l’API elle-même, à l’adresse https://api.publicwww.com/. Dans un assistant IA, les mêmes opérations sont des outils MCP.

Suivant Exemples de code