ZAPSguide

Partner API

Read live campaigns, enrol traders into your clan, and mint a member's referral link.

Live now

Five endpoints, JSON over HTTPS, server-side only. Read live campaigns, put traders into your clan without making them click a link, and hand each member their own referral link inside your own product.

SettingValue
Base URLhttps://api.zaps.wtf/zaps/v1
Auth headerx-api-key: zaps_pk_live_…
Rate limit120 requests a minute, per key

Getting a key

Keys are issued by hand, one per clan. Ask in the Telegram group and say which clan you run and what you want to build.

Ask for a key

Authentication

Send the key in a header. Either form works:

x-api-key: zaps_pk_live_...
Authorization: Bearer zaps_pk_live_...

A key in a query string is rejected with partner_key_missing. Keys are environment-bound: a zaps_pk_test_ key does not resolve against mainnet.

Endpoints

MethodPathScopePurpose
GET/partner/mecampaigns:readYour key, clan and scopes
GET/partner/campaignscampaigns:readAll campaigns you may show
GET/partner/campaigns/{slug}campaigns:readOne campaign
POST/partner/enrollclan:enrollPut traders in your clan
POST/partner/invite-linkclan:referralsA member's referral link

GET /partner/me

Tells you what your key is and what it can do. It returns the clan the key is attached to, your invite code, the scopes the key holds, and the rate limit it is given.

Call it once when your service starts. Use it to check the key is live before you depend on it, and to read inviteCode and clanUrl rather than hard-coding either. It takes no parameters and writes nothing.

{
  "partner": {
    "name": "Your clan",
    "keyPrefix": "zaps_pk_live_1a2b3c4d",
    "clanSlug": "yourclan",
    "clanName": "Your clan",
    "clanUrl": "https://app.zaps.wtf/creator/yourclan?invite=zWcURb89",
    "clanDisbanded": false,
    "inviteCode": "zWcURb89",
    "scopes": ["campaigns:read", "clan:enroll", "clan:referrals"],
    "rateLimitPerMinute": 120
  }
}

keyPrefix is safe to log. Put it in your own error reports to identify the key without sending it.

GET /partner/campaigns

Every campaign your key may show, with its current phase, its reward, its live progress figures, and how your own clan is doing in it. This is the endpoint your integration polls to render a campaign and keep its numbers current.

The list route returns them all. The detail route, GET /partner/campaigns/{slug}, returns one by slug in the identical shape, for when you already know which campaign you are rendering.

The list returns { partner, campaigns[], meta }. The detail route returns { campaign } with an identical object, or 404 campaign_not_found. Both send Cache-Control: private, max-age=30.

{
  "id": "afa636f0-4869-43c4-b6d1-7bc759c5ea43",
  "slug": "the-aerodrome-campaign",
  "name": "The Aerodrome campaign",
  "summary": "10,000,000 $AEROSTRAT, shared out among everyone who has ever traded AERO.",
  "url": "https://app.zaps.wtf/campaign/the-aerodrome-campaign",
  "registerUrl": "https://app.zaps.wtf/campaign/the-aerodrome-campaign?invite=zWcURb89",

  "status": "registration",
  "statusLabel": "Registration open",
  "phase": {
    "current": "registration",
    "registrationOpensAt": "2026-09-08T22:44:39.107Z",
    "liveAt": "2026-09-13T22:30:57.000Z",
    "endsAt": "2026-09-20T22:30:57.000Z"
  },

  "sponsor": { "name": "Aerodrome" },
  "chain":   { "name": "base", "chainId": 8453 },
  "token":   { "address": "0x940181a9…fd98631", "symbol": "AERO", "decimals": 18 },

  "reward": {
    "symbol": "AEROSTRAT",
    "poolAmount": "10000000",
    "isPlaceholder": false,
    "disclaimer": null
  },

  "split":       { "leaderPct": 20 },
  "eligibility": { "mode": "lifetime_aero", "description": "Anyone who has ever traded AERO on Base." },
  "progress":    { "registeredTraders": 944, "unlockedPct": 0 },
  "yourClan":    { "registered": 4, "traders": 0, "rank": null, "note": null },

  "meta": { "cacheSeconds": 30 }
}
FieldTypeNotes
iduuidStable. Use as your join key, not slug.
slugstringURL-safe name. This is what you pass as campaign to enrol.
summarystringOne sentence, safe to render as-is.
urlstringCampaign page, no attribution.
registerUrlstringThe link to publish. Same page, your invite code attached.
statusenumapproved · registration · live · verifying · ended
statusLabelstringThe same value in words.
phase.*ISO 8601 or nullUTC. Any date may be null before it is set.
reward.poolAmountstringWhole tokens as a decimal string. It does not always fit a JS number.
reward.isPlaceholderbooltrue means the reward token is a stand-in. Show the disclaimer, or do not publish the campaign.
reward.disclaimerstring or nullThe line to show when isPlaceholder is true.
split.leaderPctintPercentage of the pool paid to clan leaders.
eligibility.descriptionstringPlain sentence, printable as-is. mode is the machine value.
progress.registeredTradersintHeadcount across all clans. Real from the first sign-up.
progress.unlockedPctnumber0–100.
yourClan.registeredintYour people who have registered. Show this during registration.
yourClan.tradersintYour people who have traded and been scored. 0 until live.
yourClan.rankint or nullnull until someone registers.
yourClan.notestring or nullPlain-English fallback when there is nothing to rank.
meta.cacheSecondsintHow long the payload is held. Polling faster returns identical bytes.

Poll once a minute or less often. Nothing in this feed returns a wallet address.

POST /partner/enroll

Puts traders into your clan, and into a campaign if you name one. Give it a wallet address and it finds or creates that person's account, joins them to your clan, and registers them.

Use it when you already hold the addresses, so your users do not have to click a link to take part. One call takes a single person or up to 100. It is idempotent, so re-running the same call is safe and creates nothing twice.

Request

Send exactly one of wallet or users. campaign sits on the envelope and applies to every person in the call.

// one person
{ "wallet": "0x7b21…b194", "xHandle": "someone", "campaign": "the-aerodrome-campaign" }

// up to 100
{ "campaign": "the-aerodrome-campaign",
  "users": [ { "wallet": "0x7b21…b194", "xHandle": "someone" }, { "wallet": "0x9f04…1234" } ] }
FieldRequiredNotes
walletyes*^0x[0-9a-fA-F]{40}$. *Or users, never both.
users[]yes*1–100 items, each { wallet, xHandle? }.
xHandleno1–64 chars. Stored as a label only; never resolved into an identity. An X handle alone cannot enrol anyone.
campaignnoA campaign slug, 1–128 chars. Omit to enrol into the clan only.

Response

Always 200 with the same shape, single or batch. Results are positional, so results[i] answers users[i], and wallet is echoed lowercased.

{
  "enrolled": 1,
  "failed": 1,
  "clanSlug": "yourclan",
  "campaign": "the-aerodrome-campaign",
  "results": [
    { "wallet": "0x7b21…b194", "ok": true,
      "createdAccount": true, "joinedClan": true,
      "primaryClan": "set", "registered": true,
      "campaignClan": "ours", "confirmed": false },
    { "wallet": "0x9f04…1234", "ok": false,
      "error": "own_clan",
      "message": "That wallet belongs to the clan leader" }
  ]
}
FieldValuesNotes
okboolEverything below is present only when true.
createdAccountboolfalse means the address already had an account and we used it untouched.
joinedClanbooltrue also on a re-run of an existing member.
primaryClanset · already_ours · left_aloneleft_alone means it already pointed at another clan and was not taken.
registeredboolAbsent if you named no campaign.
campaignClanours · someone_else · noneWhich clan this campaign registration is credited to. Read this, not registered, to know whether the campaign counts for you.
confirmedboolfalse until that person signs in themselves. Unconfirmed members are on your roster but are not scored.

Errors

Request-level failures are 400 in the standard envelope and nothing is enrolled.

codeCause
wallet_requiredNeither wallet nor users sent.
wallet_and_usersBoth sent.
wallet_invalidAn address is not 0x + 40 hex. Rejects the whole call.
batch_emptyusers was an empty array.
batch_too_largeMore than 100 items.
batch_over_rate_limitThe call costs more than your whole per-minute limit. Not charged, so a resized retry has the full budget.
invalid_requestAnother field failed validation. The message names it.
campaign_not_foundNo campaign with that slug.
campaign_closedThat campaign is not taking registrations.
partner_has_no_clanYour key is not attached to a clan.
partner_clan_disbandedThe clan your key belongs to has been disbanded.

Everything else is per item. The call is 200 even if every item failed. The outcome lives in each ok.

errorCauseAction
own_clanThe address is your own clan leader's.Drop it.
burn_addressThe address is 0x0000…0000.Drop it, or fill it in.
clan_join_pendingYour clan is private; the join is queued.Approve it, or make the clan public and re-run.
account_failed clan_join_failed registration_failedOur side.Retry those items.

Treat an unrecognised error as a retryable per-item failure. The list is not closed. A later step failing keeps what the earlier ones did. registration_failed means the person is in your clan but has no campaign seat. Retrying an item is always safe.

POST /partner/invite-link

Returns one of your clan members' own referral link, plus how that link has performed. Use it to show a member their personal invite link inside your product.

One person per call, named by wallet address. The first call mints their code; every call after returns the same one, so it is safe to call on every page render. The response is no-store.

{ "wallet": "0x1f2e3d4c5b6a7988796a5b4c3d2e1f0091827364" }
{
  "wallet": "0x1f2e…7364",
  "clanSlug": "yourclan",
  "inviteCode": "k7Qm2Vd9",
  "inviteUrl": "https://app.zaps.wtf/creator/yourclan?invite=k7Qm2Vd9",
  "invites": 4,
  "zapsEarned": 40,
  "rewardPerInvite": 10,
  "dailyRewardCap": 10
}
FieldNotes
inviteCodeStable per member.
inviteUrlReady to render. Opens your clan page with the join set up.
invitesPeople who have joined your clan through their link, ever.
zapsEarnedWhat the link has earned that member.
rewardPerInviteCurrent payout for one successful invite.
dailyRewardCapMost invites paid to one member per UTC day.
HTTPcodeCause
400wallet_invalidwallet missing or malformed.
400partner_has_no_clanYour key is not attached to a clan.
400partner_clan_disbandedThat clan has been disbanded.
403scope_deniedYour key lacks clan:referrals.
404not_a_memberNobody in your clan holds that address. Enrol them first, then ask again.

The address goes in the body, not the path, so it stays out of access and proxy logs. Checksummed or lower-case both work; responses are lower-case.

GET /clans/{slug}/members

Public. No key, no scope.

Lists who is in a clan. Use it to render your roster, and to confirm that an enrolment landed.

100 rows a page, oldest join first. When nextCursor comes back non-null, pass it back verbatim as ?cursor= for the next page; do not build a cursor yourself. Cached public, s-maxage=15.

{
  "items": [
    { "handle": "yourclan", "name": "Your clan", "avatarUrl": "https://…",
      "role": "kol", "identityType": "x", "xConnected": true,
      "traderScore": 0, "traderTier": "Unproven", "zapsBalance": 0,
      "walletAddress": null, "pendingConfirmation": false, "minted": false,
      "joinedAt": "2026-09-09T18:52:16.309Z" },
    { "handle": null, "name": null, "avatarUrl": null,
      "role": "member", "identityType": "wallet", "xConnected": false,
      "traderScore": 36, "traderTier": "Unproven", "zapsBalance": 146,
      "walletAddress": "0x7b21…b194", "pendingConfirmation": true, "minted": false,
      "joinedAt": "2026-08-27T18:43:47.607Z" }
  ],
  "memberCount": 4,
  "unclaimedCount": 0,
  "nextCursor": null
}
FieldNotes
identityTypex has handle, name and avatar, and walletAddress is null. wallet has no handle, so the address is the public identity and is returned in full.
pendingConfirmationtrue means you enrolled them and nobody has signed in yet. They are a real member and count in memberCount.
memberCountReal members, whether X, wallet or partner-enrolled. This is the number to show.
unclaimedCountPlaceholder rows nobody enrolled and nobody has signed into. Not members.

This route returns full wallet addresses for wallet-only members. Render them shortened (0x7b21…b194). Do not export, index, cross-reference or forward them. If you only need counts, use memberCount here and yourClan.registered in the campaign feed instead.

Errors

Every error on /partner/* is the same shape, including field validation:

{ "error": { "code": "partner_key_invalid",
             "message": "That key is not valid, has expired, or has been revoked" } }
HTTPcodeAction
400malformed_jsonBody is not valid JSON.
401partner_key_missingNo key sent, or sent in the query string.
401partner_key_invalidUnknown, expired or revoked. Do not retry.
403scope_deniedKey lacks the scope. The message names it. Do not retry.
404campaign_not_foundUnknown slug.
404not_foundCampaigns are switched off our side. Retry with backoff.
429partner_rate_limitedHonour Retry-After.

Route-level 404s, meaning a path that matches no endpoint or the wrong method, return the framework's { statusCode, error, message } instead. Check the status, not the code.

Notes

Server-side only. api.zaps.wtf sends no CORS headers for partner origins. Call it from your backend and cache the result.

No webhooks. Poll the feed.

No removals. Leaving a clan is the member's own action; no key can do it for them.

The same address twice in one batch is two results and counts twice in enrolled. De-duplicate before reconciling against your own headcount.

Scopes

A route your key lacks the scope for returns 403 scope_denied. GET /partner/me lists what you hold.

ScopeOpens
campaigns:read/partner/me, /partner/campaigns, /partner/campaigns/{slug}
clan:enrollPOST /partner/enroll
clan:referralsPOST /partner/invite-link

Rate limit

120 requests a minute, counted per key across all your servers. Enrolment is charged per person, not per request, so a 100-item batch costs 100. Over the limit is 429 with a Retry-After header in seconds; honour it rather than guessing.

Ask us

Key rotation, extra scopes, a higher rate limit, or anything the doc does not answer.

Telegram group
Five endpoints · JSON over HTTPS · server-side only

On this page