Schnellstart: vom Schlüssel zum kontaktierten Partner
Der gesamte Ablauf der Partnergewinnung über die REST-API, in etwa zehn Minuten: acht Aufrufe, jeweils in curl, Node.js und Python, mit der Form der Antwort. Nur ein Schritt verbraucht Credits, und er fragt vorher.
Bevor Sie beginnen
- Ein GetAff-Konto mit mindestens einer Website. Jede Partnersuche bezieht sich auf eine Website, und Websites werden in der Anwendung angelegt.
- Ein API-Schlüssel mit den Geltungsbereichen
read,search,writeundspend, erstellt auf der Seite „API-Schlüssel“. Ohnespendfunktioniert alles außer dem kostenpflichtigen Schritt 7. - Der Tarif Pro für Schritt 7: Kostenpflichtige E-Mail-Suchen (wie Live-Suchen) sind im kostenlosen Tarif nicht enthalten, der mit 403
plan_requiredantwortet. - Für Schritt 8, einmalig in der Anwendung: ein verbundenes Postfach, vollständige Absendereinstellungen (Name, Unternehmen, Postanschrift) und ein veröffentlichtes Programm.
Einrichtung
Bewahren Sie den Schlüssel in einer Umgebungsvariable auf einem Server auf — niemals in einem Browser. Die Beispiele für Node.js und Python verwenden die kleine Hilfsfunktion unten: Sie sendet den Schlüssel, kodiert den Body als JSON, setzt Idempotency-Key, wenn einer übergeben wird, und löst bei jedem Fehler außer 402, den Schritt 7 behandelt, eine Ausnahme aus. Fehler sind application/problem+json: Entscheiden Sie anhand von error.code, niemals anhand der Meldung.
export GETAFF_API_KEY='gaff_live_…' # scopes: read search write spend
BASE=https://www.getaff.org/api
SITE=Wq3b9ZK1 # the site id from step 1// Node.js 18+: fetch and crypto.randomUUID are built in.
const BASE = 'https://www.getaff.org/api';
async function getaff(method, path, { body, idempotencyKey } = {}) {
const res = await fetch(BASE + path, {
method,
headers: {
Authorization: `Bearer ${process.env.GETAFF_API_KEY}`,
...(body ? { 'Content-Type': 'application/json' } : {}),
...(idempotencyKey ? { 'Idempotency-Key': idempotencyKey } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
const json = await res.json();
// Errors are application/problem+json: branch on error.code, never on the message.
if (!res.ok && res.status !== 402) {
throw new Error(`${res.status} ${json.error.code}: ${json.detail}`);
}
return { status: res.status, json };
}# pip install requests
import os
import uuid
import requests
BASE = "https://www.getaff.org/api"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['GETAFF_API_KEY']}"
def getaff(method, path, body=None, idempotency_key=None):
headers = {"Idempotency-Key": idempotency_key} if idempotency_key else {}
r = session.request(method, BASE + path, json=body, headers=headers, timeout=90)
payload = r.json()
# Errors are application/problem+json: branch on error.code, never on the message.
if not r.ok and r.status_code != 402:
raise RuntimeError(f"{r.status_code} {payload['error']['code']}: {payload['detail']}")
return r.status_code, payloadJeder Schreibzugriff in dieser Anleitung akzeptiert einen Idempotency-Key-Header (8 bis 128 Zeichen aus A-Z a-z 0-9 _ - : .; eine UUID passt). Eine Wiederholung mit demselben Schlüssel und demselben Body innerhalb von 24 Stunden spielt den ersten Erfolg mit Idempotent-Replayed: true erneut aus, statt noch einmal zu laufen; derselbe Schlüssel mit einem anderen Body ergibt 409 idempotency_conflict. Fehler werden nicht gespeichert: Eine korrigierte Anfrage kann ihren Schlüssel weiterverwenden. Ein Schlüssel je Vorgang, wiederverwendet bei dessen Wiederholungen.
1. Ihre Websites auflisten
GET /v1/sites liefert die Websites Ihres Kontos, jeweils mit dem Partnerprogramm, das sie auf GetAff anbietet. Merken Sie sich die id der Website, für die Sie Partner gewinnen: Alle folgenden Aufrufe verwenden sie.
curl -s "$BASE/v1/sites" \
-H "Authorization: Bearer $GETAFF_API_KEY"const { json: sites } = await getaff('GET', '/v1/sites');
const siteId = sites.data[0].id;_, sites = getaff("GET", "/v1/sites")
site_id = sites["data"][0]["id"]{
"data": [
{
"id": "Wq3b9ZK1",
"host": "nyna.ai",
"url": "https://nyna.ai",
"name": "Nyna",
"createdAt": "2026-09-02T10:11:00.000Z",
"programme": {
"name": "Nyna Partners",
"slug": "nyna-partners",
"markets": [
"FR",
"BE"
],
"model": "cps"
}
}
]
}2. Die Suchkriterien festlegen
PATCH /v1/sites/{id}/criteria (Geltungsbereich write) ersetzt jedes gesendete Feld und behält die übrigen: Schlüsselwörter, Nischen, Plattformen, Länder, Sprachen, Reichweitenklassen, Aktualität. { "reset": true } kehrt zu den Kriterien zurück, die GetAff aus der Website ableitet. Überspringen Sie diesen Schritt, um mit diesen abgeleiteten Kriterien zu suchen.
curl -s -X PATCH "$BASE/v1/sites/$SITE/criteria" \
-H "Authorization: Bearer $GETAFF_API_KEY" \
-H 'Content-Type: application/json' \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"keywords":["wedding planning","bridal"],"countries":["FR","BE"]}'await getaff('PATCH', `/v1/sites/${siteId}/criteria`, {
body: { keywords: ['wedding planning', 'bridal'], countries: ['FR', 'BE'] },
idempotencyKey: crypto.randomUUID(),
});getaff(
"PATCH",
f"/v1/sites/{site_id}/criteria",
{"keywords": ["wedding planning", "bridal"], "countries": ["FR", "BE"]},
str(uuid.uuid4()),
){
"data": {
"criteria": {
"keywords": [
"wedding planning",
"bridal"
],
"niches": [
"weddings"
],
"platforms": [
"instagram",
"youtube"
],
"countries": [
"FR",
"BE"
],
"languages": [
"fr"
],
"bands": [
"micro",
"mid"
],
"activeWithinDays": 90
},
"edited": true,
"droppedNiches": []
}
}3. Eine Partnersuche starten
POST /v1/sites/{id}/partner-search (Geltungsbereich search) führt die Suche der Anwendung aus: den Creator-Index von GetAff, gerankt für Ihr Programm, ähnliche Profile zu Ihrer Auswahl und die Creator, die Ihre Wettbewerber bewerben — zusammengeführt, dedupliziert und gerankt. Ohne includeLive verbraucht sie keine Credits. Mit includeLive: true fragt sie zusätzlich die Live-Datenanbieter ab: Dafür sind spend und ein confirmCost nötig, genau wie in Schritt 7. statuses zeigt, was jede Quelle getan hat.
curl -s -X POST "$BASE/v1/sites/$SITE/partner-search" \
-H "Authorization: Bearer $GETAFF_API_KEY" \
-H 'Content-Type: application/json' \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"limit":50}'const { json: run } = await getaff('POST', `/v1/sites/${siteId}/partner-search`, {
body: { limit: 50 },
idempotencyKey: crypto.randomUUID(),
});
console.log(run.data.total, run.data.statuses.map((s) => `${s.id}: ${s.status}`));_, run = getaff(
"POST", f"/v1/sites/{site_id}/partner-search", {"limit": 50}, str(uuid.uuid4())
)
print(run["data"]["total"], [(s["id"], s["status"]) for s in run["data"]["statuses"]]){
"data": {
"siteId": "Wq3b9ZK1",
"total": 58,
"statuses": [
{
"id": "index",
"kind": "index",
"status": "ok",
"count": 42
}
],
"creditsSpent": 0,
"searchId": "mh7c2k0qx4f9za"
}
}4. Die gerankten Ergebnisse lesen
GET /v1/sites/{id}/partner-search/latest blättert durch den letzten Lauf (30 Tage aufbewahrt; jeder Lauf wird außerdem unter /partner-search/history gespeichert). Jedes Ergebnis trägt seinen key, einen Passungswert fit.pfs von 0 bis 100 und fit.reasons, die Begründung seines Rangs. minPfs behält nur die starken Treffer; folgen Sie page.nextCursor für weitere.
curl -s "$BASE/v1/sites/$SITE/partner-search/latest?minPfs=60&limit=50" \
-H "Authorization: Bearer $GETAFF_API_KEY"const { json: latest } = await getaff(
'GET',
`/v1/sites/${siteId}/partner-search/latest?minPfs=60&limit=50`,
);
for (const r of latest.data.results) {
console.log(r.rank, r.handle, r.fit.pfs, r.fit.reasons.map((x) => x.code));
}
const keys = latest.data.results.slice(0, 10).map((r) => r.key);_, latest = getaff("GET", f"/v1/sites/{site_id}/partner-search/latest?minPfs=60&limit=50")
for r in latest["data"]["results"]:
print(r["rank"], r["handle"], r["fit"]["pfs"], [x["code"] for x in r["fit"]["reasons"]])
keys = [r["key"] for r in latest["data"]["results"][:10]]{
"data": {
"total": 58,
"results": [
{
"key": "instagram:1784140",
"rank": 1,
"platform": "instagram",
"handle": "lea.mariage",
"displayName": "Léa · Mariage",
"audience": 48200,
"country": "FR",
"fit": {
"pfs": 86,
"excluded": false,
"reasons": [
{
"code": "topic",
"terms": [
"mariage"
],
"host": null
}
]
}
}
]
},
"page": {
"limit": 50,
"hasMore": true,
"nextCursor": "NTA"
}
}5. Die besten auswählen
POST /v1/sites/{id}/shortlist (Geltungsbereich write) übernimmt Ergebnisse der letzten Suche per key, bis zu 50 je Aufruf; die Auswahl fasst höchstens 100. Ein bereits ausgewählter Creator wird aktualisiert, nicht verdoppelt; ein Schlüssel, der nicht in den letzten Ergebnissen steht, kommt in unknownKeys zurück, und für ihn wird nichts übernommen.
curl -s -X POST "$BASE/v1/sites/$SITE/shortlist" \
-H "Authorization: Bearer $GETAFF_API_KEY" \
-H 'Content-Type: application/json' \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"keys":["instagram:1784140"]}'const { json: shortlist } = await getaff('POST', `/v1/sites/${siteId}/shortlist`, {
body: { keys },
idempotencyKey: crypto.randomUUID(),
});
console.log(shortlist.added, shortlist.unknownKeys);_, shortlist = getaff(
"POST", f"/v1/sites/{site_id}/shortlist", {"keys": keys}, str(uuid.uuid4())
)
print(shortlist["added"], shortlist["unknownKeys"]){
"data": [
{
"key": "instagram:1784140",
"handle": "lea.mariage",
"pfs": 86,
"status": "selected",
"prospectId": null
}
],
"max": 100,
"added": 1,
"full": false,
"unknownKeys": []
}6. Zu Interessenten machen
POST /v1/sites/{id}/shortlist/to-prospects (Geltungsbereich write) speichert ausgewählte Creator als Ihre privaten Interessenten. Es werden keine Credits verbraucht, der Vorgang zählt aber auf das Entdeckungskontingent Ihres Tarifs (quota). Ohne keys werden alle ausgewählten Creator übernommen, die noch keine Interessenten sind. Ein kostenloser Blick in die öffentliche Bio findet manchmal schon eine Adresse (enriched). Merken Sie sich prospectIds.
curl -s -X POST "$BASE/v1/sites/$SITE/shortlist/to-prospects" \
-H "Authorization: Bearer $GETAFF_API_KEY" \
-H 'Content-Type: application/json' \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"keys":["instagram:1784140"]}'const { json: written } = await getaff(
'POST',
`/v1/sites/${siteId}/shortlist/to-prospects`,
{ body: { keys }, idempotencyKey: crypto.randomUUID() },
);
const prospectIds = written.prospectIds;_, written = getaff(
"POST",
f"/v1/sites/{site_id}/shortlist/to-prospects",
{"keys": keys},
str(uuid.uuid4()),
)
prospect_ids = written["prospectIds"]{
"written": 1,
"alreadyInProspects": 0,
"enriched": 0,
"prospectIds": [
"kJ2p8wQxYb_ig_1784140"
],
"quota": {
"used": 141,
"limit": 2000
}
}7. E-Mail-Adressen finden (kostenpflichtig)
POST /v1/prospects/find-emails (Geltungsbereich spend, Tarif Pro) sucht bei den Datenanbietern eine Adresse für Interessenten, die noch keine haben. Eine vorhandene Adresse wird nie ersetzt, und die Sperrliste gilt. Da Credits verbraucht werden, sind zwei Aufrufe nötig.
IDEM=$(uuidgen) # one key for the whole operation
# 7a. No confirmCost: 402 with the estimate, nothing spent
curl -s -X POST "$BASE/v1/prospects/find-emails" \
-H "Authorization: Bearer $GETAFF_API_KEY" \
-H 'Content-Type: application/json' \
-H "Idempotency-Key: $IDEM" \
-d '{"ids":["kJ2p8wQxYb_ig_1784140"]}'
# 7b. A human agreed to error.details.estimate.credits: confirm, same key
curl -s -X POST "$BASE/v1/prospects/find-emails" \
-H "Authorization: Bearer $GETAFF_API_KEY" \
-H 'Content-Type: application/json' \
-H "Idempotency-Key: $IDEM" \
-d '{"ids":["kJ2p8wQxYb_ig_1784140"],"confirmCost":1}'const idempotencyKey = crypto.randomUUID(); // one key for the whole operation
const ask = await getaff('POST', '/v1/prospects/find-emails', {
body: { ids: prospectIds },
idempotencyKey,
});
if (ask.status === 402 && ask.json.error.code === 'cost_confirmation_required') {
const { credits } = ask.json.error.details.estimate;
// askHuman: your own approval step (a prompt, a Slack button, a ticket…).
if (await askHuman(`Look up ${prospectIds.length} e-mail(s) for up to ${credits} credits?`)) {
const { json: found } = await getaff('POST', '/v1/prospects/find-emails', {
body: { ids: prospectIds, confirmCost: credits },
idempotencyKey,
});
console.log(found.found, 'found,', found.none, 'without an address');
}
}idem = str(uuid.uuid4()) # one key for the whole operation
status, ask = getaff("POST", "/v1/prospects/find-emails", {"ids": prospect_ids}, idem)
if status == 402 and ask["error"]["code"] == "cost_confirmation_required":
credits = ask["error"]["details"]["estimate"]["credits"]
if input(f"Spend up to {credits} credits? [y/N] ").strip().lower() == "y":
_, found = getaff(
"POST",
"/v1/prospects/find-emails",
{"ids": prospect_ids, "confirmCost": credits},
idem,
)
print(found["found"], "found,", found["none"], "without an address")7a — Die Schätzung anfordern
Senden Sie den Aufruf ohne confirmCost. Die Antwort ist ein 402 cost_confirmation_required mit der genauen Schätzung in error.details.estimate — und es wird nichts ausgegeben. Zeigen Sie estimate.credits einem Menschen.
HTTP/1.1 402 Payment Required
Content-Type: application/problem+json
{
"type": "https://www.getaff.org/docs/errors/cost_confirmation_required",
"title": "Cost confirmation required",
"status": 402,
"detail": "Looking up 1 address(es) would spend up to 1 credits. Nothing was spent. Resend with `confirmCost` >= `error.details.estimate.credits`.",
"instance": "urn:getaff:request:req_6f1c2a9e0b7d4c55a1f3e2d8c9b0a7e6",
"error": {
"code": "cost_confirmation_required",
"message": "Looking up 1 address(es) would spend up to 1 credits. Nothing was spent. Resend with `confirmCost` >= `error.details.estimate.credits`.",
"details": {
"estimate": {
"credits": 1,
"action": "email_lookup",
"units": 1,
"lines": [],
"remainingToday": 199
}
}
}
}7b — Bestätigen und ausführen
Sobald er zugestimmt hat, senden Sie denselben Aufruf mit einem confirmCost, der mindestens estimate.credits entspricht: Das ist der Höchstbetrag, den Sie auszugeben bereit sind. Behalten Sie denselben Idempotency-Key: Der 402 wurde nicht gespeichert, der bestätigte Aufruf läuft also; läuft er in eine Zeitüberschreitung und Sie wiederholen ihn unverändert, erhalten Sie sein erstes Ergebnis zurück (Idempotent-Replayed: true), statt doppelt zu zahlen. results[].status nennt je Interessent found, none, daily_limit …
{
"looked": 1,
"found": 1,
"none": 0,
"stopped": 0,
"skipped": 0,
"estimate": {
"credits": 1,
"action": "email_lookup",
"units": 1,
"lines": [],
"remainingToday": 199
},
"results": [
{
"prospectId": "kJ2p8wQxYb_ig_1784140",
"status": "found"
}
]
}8. In eine Sequenz aufnehmen
POST /v1/outreach/sequences (Geltungsbereich write) plant die dreistufige Sequenz — eine erste Nachricht und zwei Nachfassmails — für einen Interessenten, mit allen Prüfungen der Anwendung: ein verbundenes Postfach (geben Sie mailboxId an, wenn Sie mehrere haben; GET /v1/outreach/mailboxes listet sie auf), vollständige Absendereinstellungen, ein veröffentlichtes Programm, eine gültige Adresse, die nicht auf der Sperrliste steht, und die Compliance-Regeln des Empfängerlandes. attestations sind Ihr Nachweis der Rechtsgrundlage, wie im GetAff-Editor; ohne draft wird der für diesen Interessenten in GetAff gespeicherte Entwurf verwendet.
curl -s -X POST "$BASE/v1/outreach/sequences" \
-H "Authorization: Bearer $GETAFF_API_KEY" \
-H 'Content-Type: application/json' \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"prospectId":"kJ2p8wQxYb_ig_1784140",
"attestations":["b2b_role_relevant","legitimate_interest_assessed"]}'const { json: sequence } = await getaff('POST', '/v1/outreach/sequences', {
body: {
prospectId: prospectIds[0],
attestations: ['b2b_role_relevant', 'legitimate_interest_assessed'],
},
idempotencyKey: crypto.randomUUID(),
});
console.log(sequence.data.state, sequence.data.steps.map((s) => s.dueAt));_, sequence = getaff(
"POST",
"/v1/outreach/sequences",
{
"prospectId": prospect_ids[0],
"attestations": ["b2b_role_relevant", "legitimate_interest_assessed"],
},
str(uuid.uuid4()),
)
print(sequence["data"]["state"], [s["dueAt"] for s in sequence["data"]["steps"]]){
"data": {
"id": "kJ2p8wQxYb_ig_1784140",
"prospectId": "kJ2p8wQxYb_ig_1784140",
"mailboxEmail": "partners@nyna.ai",
"state": "active",
"regimeId": "eu_gdpr",
"steps": [
{
"key": "opening",
"state": "scheduled",
"dueAt": "2026-10-27T08:30:00.000Z",
"sentAt": null
},
{
"key": "followup1",
"state": "scheduled",
"dueAt": "2026-11-01T08:30:00.000Z",
"sentAt": null
},
{
"key": "followup2",
"state": "scheduled",
"dueAt": "2026-11-08T08:30:00.000Z",
"sentAt": null
}
]
}
}Die Aufnahme verbraucht keine Credits und nimmt daher kein confirmCost. Sie plant aber echte E-Mails aus Ihrem Postfach: Lassen Sie den Interessenten und die Nachricht vor dem Aufruf von einem Menschen freigeben — das MCP-Werkzeug enrol_in_sequence erzwingt genau das mit einer zweistufigen Bestätigung. Die API sendet selbst nie eine E-Mail; das Postfach versendet jeden Schritt zu seinem dueAt. Jede Ablehnung hat ihren eigenen Code: mailbox_required, draft_required, outreach_settings_incomplete, program_required, recipient_suppressed, compliance_blocked (mit details.missing) …
Jede Antwort trägt RateLimit-Policy und RateLimit (sowie X-RateLimit-Limit und X-RateLimit-Remaining); ein 429 trägt Retry-After, in Sekunden. Die Obergrenzen der Anwendung gelten ebenfalls — etwa 6 Partnersuchen pro Minute und Konto.
Wie es weitergeht
Abonnieren Sie den Webhook prospect.replied, um von Antworten zu erfahren, statt die API abzufragen; die Referenz beschreibt jedes Feld und jeden Fehler jedes Endpunkts; derselbe Ablauf steht einem KI-Assistenten über den MCP-Server offen.