Automating affiliate recruitment with the GetAff REST API
The GetAff REST API exposes the same operations as the partner-recruiting screens: search criteria, one search across every source, the shortlist, prospects and their e-mails, and enrolment in an outreach sequence. A nightly job can therefore refresh a search, keep the best new creators and prepare their outreach without anyone opening the app.
This article follows that job from the first request to the last, with the three rules that make an automation safe: paid calls are confirmed with a number, writes carry an idempotency key, and the client reads the rate-limit headers instead of guessing.
Updated
Create a key with the right scopes
Sign in, open the API keys page and create one key per integration — that is what lets you revoke one without stopping the others. The value is shown exactly once: GetAff stores only a digest.
readis always included: sites, criteria, results, prospects, sequences.searchfor searches that spend no credit (index, lookalikes, competitors).writeto change criteria, the shortlist, prospects and to enrol in a sequence.spendfor live provider searches and paid e-mail lookups — only for an integration you trust to respect a budget.
Keep the key server-side, in a secret manager or an environment variable. The API refuses every browser origin by design: a key in page JavaScript is a published key. Details in Authentication.
First call: your sites
The base URL is https://www.getaff.org/api/v1. Every partner search belongs to one site, so start by listing them:
curl -s https://www.getaff.org/api/v1/sites \
-H "Authorization: Bearer $GETAFF_API_KEY"Each site comes with the program it runs. GET /v1/sites/{id}/criteria returns the criteria the next search will use and those GetAff derives from the site; PATCH on the same path replaces the fields you send and keeps the others.
Run the partner search
POST /v1/sites/{id}/partner-search runs the search the app runs: the GetAff creator index ranked for your program, lookalikes of your shortlist, your competitors’ promoters and — with includeLive — the live creator-data providers. Results are merged, deduplicated and ranked, each with a fit score and its reasons; statuses tells you what each source did.
SITE_ID=Wq3b9ZK1
curl -s -X POST https://www.getaff.org/api/v1/sites/$SITE_ID/partner-search \
-H "Authorization: Bearer $GETAFF_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: nightly-$SITE_ID-$(date +%F)" \
-d '{"limit": 50}'The run replaces the latest search (kept 30 days) and is added to the saved searches, so GET /v1/sites/{id}/partner-search/latest and the history endpoints can read it again later without running anything.
Paid calls: the 402 handshake
Two operations spend provider credits: the partner search with includeLive: true and POST /v1/prospects/find-emails. Both need the spend scope AND a confirmCost at least equal to the estimate. Without it, nothing is spent and the API answers 402 cost_confirmation_required, with the exact estimate in error.details.estimate.
- Send the request without
confirmCost. - Read
error.details.estimate.creditsfrom the 402. - Compare it with your budget — or show it to a human.
- Resend the same request with
confirmCostset to that number or a ceiling above it.
async function confirmed(url, body) {
const call = (b) =>
fetch(url, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.GETAFF_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': crypto.randomUUID(),
},
body: JSON.stringify(b),
});
let res = await call(body);
if (res.status === 402) {
const { error } = await res.json();
const credits = error.details.estimate.credits;
if (credits > MAX_CREDITS_PER_RUN) throw new Error(`Over budget: ${credits}`);
res = await call({ ...body, confirmCost: credits });
}
return res.json();
}Every confirmed spend is written to an audit log (key, action, credits — never a creator’s data). Your plan’s daily allowances still apply on top of any budget you set.
From results to prospects
Each result carries a key such as instagram:1784140. Keep the ones your rules select — a minimum fit score, a country, an audience floor — and chain three calls:
POST /v1/sites/{id}/shortlistwith{"keys": [...]}adds them to the shortlist (it holds 100).POST /v1/sites/{id}/shortlist/to-prospectsturns them into private prospects. Free; it counts on your plan’s discovery quota.POST /v1/prospects/find-emailswith the prospectidslooks up the missing addresses (Pro, paid, confirmed as above). It never replaces an existing address, and the suppression list applies.
GET /v1/prospects?siteId=…&hasEmail=false lists what is still missing; the collection is cursor-paginated — pass page.nextCursor back verbatim as cursor.
Enrol in an outreach sequence
POST /v1/outreach/sequences schedules the three-step sequence (opening and two follow-ups) for one prospect, from a connected mailbox, with every check the app makes: complete sender settings, a published program, a valid and unsuppressed address, and the compliance regime of the recipient’s country.
{
"prospectId": "kJ2p8wQxYb_ig_1784140",
"attestations": ["b2b_role_relevant", "legitimate_interest_assessed"]
}Without draft, the draft saved for that prospect in GetAff is used. Each refusal has its own code — mailbox_required, draft_required, recipient_suppressed, compliance_blocked (with details.missing) and so on — so your job can log exactly why a prospect was skipped. There is no endpoint that sends an e-mail directly: sending happens later, from your mailbox, at the scheduled times.
Retry without paying twice
Writes accept an Idempotency-Key header. A retry with the same key and the same body within 24 hours replays the first success (Idempotent-Replayed: true) instead of running again — which matters for a search that spent credits. Errors are not stored: correct the request and retry with the same key.
- Derive the key from what the job means:
nightly-<siteId>-<date>, not a random value per attempt. - The same key with a different body answers
idempotency_conflict; a request still running answersidempotency_in_progress.
Rate limits and errors
Throughput is counted per key over one minute: 60 requests on Free, 600 on Pro, with an account-wide ceiling shared by all keys and the MCP server. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and the IETF RateLimit-Policy / RateLimit headers; a 429 adds Retry-After. The app’s own ceilings apply as well, such as six partner searches a minute.
Errors are RFC 9457 problem documents with a stable error.code: branch on the code, never on the message. Retry 429 and 503 (honouring Retry-After); 400, 401, 403 and 404 will not change by retrying. The full lists are in Rate limits and Errors.
Next steps
The getting-started page has the first call and the versioning rules, and the reference documents every endpoint, generated from the schemas the routes enforce. To react when a prospect replies instead of polling, add a signed webhook.
Documentation
The exact contract, generated from the schemas the routes enforce:
- Quickstart: the full flow in eight calls
- Getting started with the API
- Endpoint reference
- Authentication and scopes
- Rate limits
- Errors
Read next
Connect your AI assistant to GetAff over MCP and OAuth, then search, rank, shortlist and contact creators in plain language — with every cost and every send confirmed by you.
Receive search.finished, prospect.replied, application.received and partner.approved on your own endpoint, verify the Standard Webhooks signature in Node or Python, handle retries.
Criteria, topic terms, four sources, evidence-first ranking and saved searches: the GetAff partner search, step by step, and how to read why a creator ranks where it does.