Cluster
Ein Cluster ist eine gespeicherte Liste von Websites. Über die API können Sie einen Cluster aus einer Suche oder aus Ihrer eigenen Liste erstellen, Cluster kombinieren und Werte aus dem Quelltext jeder Website eines Clusters herausziehen - Kontakte, Social-Media-Profile, Analytics- und Tag-IDs, alles, was ein regulärer Ausdruck erfassen kann.
Die Endpunkte
| Endpunkt | Methode | Funktion |
|---|---|---|
/v1/clusters | GET | Die Cluster des Kontos, die neuesten zuerst: ID, Name, Zahl der Domains, Erstellungszeitpunkt. |
/v1/clusters | POST | Erstellt einen Cluster aus einer Suche (query) oder aus einer Liste (domains); name ist optional. |
/v1/clusters/{id} | GET | Der Cluster und eine Seite seiner Domains: page, per_page (bis zu 10.000), format=txt für eine Domain pro Zeile. |
/v1/clusters/combine | POST | Ein neuer Cluster aus bestehenden: operation and, or oder diff, clusters - eine Liste von IDs. |
/v1/clusters/{id}/extract | GET, POST | Extrahiert Werte mit presets und regex, Teil für Teil: offset, limit (bis zu 1.000 Websites pro Aufruf), format json, xml oder csv. |
/v1/clusters/presets | GET | Die fertigen Ausdrücke mit ihren Mustern. |
/v1/clusters/{id}/rename | POST | Neuer name. |
/v1/clusters/{id}/delete | POST | Löscht den Cluster endgültig. |
Alles, was einen Cluster verändert, ist ein POST mit JSON-Body - kein PUT oder
DELETE, sodass jeder Client, der suchen kann, auch Cluster verwalten kann. Token,
Ratenlimit und Fehlerformat sind dieselben wie bei der
Suche; die Cluster-Angaben in der Antwort von
/v1/account zeigen, wie viele Cluster Sie haben und wie viele
Extraktionspunkte heute noch übrig sind.
Einen Cluster erstellen
Aus einer Suche: Die Ergebnisse der Suchanfrage werden zum Cluster, bis zur
maximalen Zeilenzahl Ihres Tarifs. Das kostet eine Suche, genau wie
/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}
Aus einer Liste: Domains oder URLs, als JSON-Array oder eine pro Zeile.
Übernommen werden nur Websites, die im Index sind - submitted gibt an,
wie viele Sie gesendet haben, size, wie viele übernommen wurden. Eine
Liste kostet nichts.
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"}'
Der Tarif begrenzt die Zahl der Domains in einem Cluster, und ein Konto behält bis
zu 100 Cluster. Sind bereits 100 vorhanden, wird das Erstellen eines weiteren mit
409 cluster_limit beantwortet - in Ihrem Namen wird nichts gelöscht;
löschen Sie zuerst die Cluster, die Sie nicht mehr brauchen.
Cluster kombinieren
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 behält die Domains, die in jedem der Cluster sind, or
die, die in mindestens einem sind, und diff die, die im ersten Cluster
sind, aber nicht im zweiten - dafür genau zwei Cluster. Kombinieren kostet nichts.
Daten extrahieren
Die Extraktion liest die indexierten Seiten jeder Website im Cluster und liefert, was die Ausdrücke erfassen, eine Spalte pro Ausdruck. Am einfachsten geht es mit einer Vorlage:
| Vorlage | Was sie herausholt |
|---|---|
email | Adressen aus mailto:-Links |
phone | Nummern aus tel:-Links |
whatsapp, telegram, skype | WhatsApp-Nummern, Telegram-Benutzernamen und Skype-Namen aus den jeweiligen Links |
facebook, instagram, twitter, linkedin | Links zu den Social-Media-Profilen der Website (twitter erfasst auch x.com, linkedin auch Unternehmensseiten) |
gtm, ga4, ua | Container-IDs von Google Tag Manager sowie IDs von Google Analytics 4 und Universal Analytics |
hotjar | Hotjar-Site-ID |
adsense | AdSense-Publisher-ID |
bitcoin | Adressen aus bitcoin:-Zahlungslinks |
Oder Sie schreiben ein eigenes Muster: einen regulären Ausdruck zwischen
Schrägstrichen (oder senkrechten Strichen) mit den optionalen Flags i,
m, s, u, bis zu 200 Zeichen lang. Die erste
Erfassungsgruppe ist der Wert - dieselbe Regel wie bei
snipexp:. Bis zu zehn
Ausdrücke pro Aufruf, Vorlagen eingeschlossen.
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 }
]
}
Teil für Teil. Ein Aufruf geht bis zu 1.000 Websites des Clusters durch,
beginnend bei offset. Rufen Sie dann erneut auf und setzen Sie
offset auf next_offset, bis dieser Wert null
ist. Websites ohne Treffer werden weggelassen; mit skip_empty=0 werden
auch sie aufgeführt. format=csv liefert eine Zeile pro Website - die
Domain, dann eine Spalte pro Ausdruck - und den nächsten Offset im Header
X-Next-Offset.
Punkte. Die Extraktion verbraucht das tägliche Extraktionskontingent Ihres
Tarifs: Jede im Index gefundene Website kostet so viele Punkte, wie Werte auf ihr
gefunden wurden, 0,1, wenn keine. Sind die Punkte aufgebraucht, bricht der Aufruf
vorzeitig mit "stopped": "extract_quota_exceeded" ab und liefert, was
er bis dahin hat; ein Aufruf ohne verbleibende Punkte erhält
429 extract_quota_exceeded. Probieren Sie einen Ausdruck zuerst mit
einem kleinen limit aus - siehe
Cluster-Limits.
Ein ganzer Cluster im Code
Einen Cluster aus einer Suche erstellen und die extrahierten Werte jeder Website in eine CSV-Datei schreiben. Die Client-Bibliotheken bieten dasselbe als fertige Funktionen und auf der Kommandozeile.
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
Fehler
| Status und Code | Bedeutung |
|---|---|
404 cluster_not_found | Kein Cluster mit dieser ID in diesem Konto. |
409 cluster_limit | Das Konto hat bereits 100 Cluster. |
400 invalid_regex | Ein Ausdruck ist kein PCRE-Ausdruck zwischen Schrägstrichen oder senkrechten Strichen mit bis zu 200 Zeichen. |
400 unknown_preset | Keine solche Vorlage; die Antwort listet die vorhandenen auf. |
400 missing_source, ambiguous_source | Zum Erstellen ist entweder query oder domains nötig. |
429 extract_quota_exceeded | Die Extraktionspunkte für heute sind aufgebraucht. |
Die vollständige Liste steht auf der Seite Fehler und in der Selbstbeschreibung der API unter https://api.publicwww.com/. In einem KI-Assistenten stehen dieselben Operationen als MCP-Tools bereit.
Weiter Codebeispiele