GetAff
API-Dokumentation

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, write und spend, erstellt auf der Seite „API-Schlüssel“. Ohne spend funktioniert 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_required antwortet.
  • Für Schritt 8, einmalig in der Anwendung: ein verbundenes Postfach, vollständige Absendereinstellungen (Name, Unternehmen, Postanschrift) und ein veröffentlichtes Programm.
API-Schlüssel

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.

curl
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
// 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 };
}
Python (requests)
# 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, payload
Idempotenz, bei jedem Schreibzugriff

Jeder 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
curl -s "$BASE/v1/sites" \
  -H "Authorization: Bearer $GETAFF_API_KEY"
Node.js
const { json: sites } = await getaff('GET', '/v1/sites');
const siteId = sites.data[0].id;
Python (requests)
_, sites = getaff("GET", "/v1/sites")
site_id = sites["data"][0]["id"]
Antwort (gekürzt)
{
  "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
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"]}'
Node.js
await getaff('PATCH', `/v1/sites/${siteId}/criteria`, {
  body: { keywords: ['wedding planning', 'bridal'], countries: ['FR', 'BE'] },
  idempotencyKey: crypto.randomUUID(),
});
Python (requests)
getaff(
    "PATCH",
    f"/v1/sites/{site_id}/criteria",
    {"keywords": ["wedding planning", "bridal"], "countries": ["FR", "BE"]},
    str(uuid.uuid4()),
)
Antwort (gekürzt)
{
  "data": {
    "criteria": {
      "keywords": [
        "wedding planning",
        "bridal"
      ],
      "niches": [
        "weddings"
      ],
      "platforms": [
        "instagram",
        "youtube"
      ],
      "countries": [
        "FR",
        "BE"
      ],
      "languages": [
        "fr"
      ],
      "bands": [
        "micro",
        "mid"
      ],
      "activeWithinDays": 90
    },
    "edited": true,
    "droppedNiches": []
  }
}

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
curl -s "$BASE/v1/sites/$SITE/partner-search/latest?minPfs=60&limit=50" \
  -H "Authorization: Bearer $GETAFF_API_KEY"
Node.js
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);
Python (requests)
_, 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]]
Antwort (gekürzt)
{
  "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
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"]}'
Node.js
const { json: shortlist } = await getaff('POST', `/v1/sites/${siteId}/shortlist`, {
  body: { keys },
  idempotencyKey: crypto.randomUUID(),
});
console.log(shortlist.added, shortlist.unknownKeys);
Python (requests)
_, shortlist = getaff(
    "POST", f"/v1/sites/{site_id}/shortlist", {"keys": keys}, str(uuid.uuid4())
)
print(shortlist["added"], shortlist["unknownKeys"])
Antwort (gekürzt)
{
  "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
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"]}'
Node.js
const { json: written } = await getaff(
  'POST',
  `/v1/sites/${siteId}/shortlist/to-prospects`,
  { body: { keys }, idempotencyKey: crypto.randomUUID() },
);
const prospectIds = written.prospectIds;
Python (requests)
_, written = getaff(
    "POST",
    f"/v1/sites/{site_id}/shortlist/to-prospects",
    {"keys": keys},
    str(uuid.uuid4()),
)
prospect_ids = written["prospectIds"]
Antwort (gekürzt)
{
  "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.

curl
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}'
Node.js
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');
  }
}
Python (requests)
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.

402
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 …

200 OK
{
  "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
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"]}'
Node.js
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));
Python (requests)
_, 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"]])
Antwort (gekürzt)
{
  "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
      }
    ]
  }
}
Keine Credits, aber echte E-Mails

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) …

Ratenbegrenzung

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.

Ratenbegrenzung

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.