The SkyFollowing API.
Create campaigns, adjust pacing, read attribution, and queue profiles from your own tools. Everything the dashboard does to campaigns, your scripts can do too.
Authentication
Create a key on the dashboard under API keys: name it, generate, and copy it once. Keys look like skg_… and are stored as a SHA-256 hash, so the full key is only ever shown at creation.
Send it on every request as a bearer token. Keys are scoped to your workspace, track their last use, and can be revoked instantly from the same page. The public API requires the Agency plan; other plans receive a 403.
curl https://skyfollowing.com/api/ext/v1/campaigns \ -H "Authorization: Bearer skg_4f19c2…"
Errors and rate limits
Every error returns JSON in one shape: { "error": "message" }. Rate limiting is per key, per minute window; back off and retry after a 429.
| Status | When |
|---|---|
| 400 | Validation failed; the message names the first invalid field |
| 401 | Missing bearer token, or the key is invalid or revoked |
| 403 | Plan does not include API access, or a plan limit was exceeded |
| 404 | Campaign or Bluesky profile not found in your workspace |
| 429 | More than 100 requests in a minute on this key |
Pick an endpoint, fill the fields, copy the curl.
curl -X POST https://skyfollowing.com/api/ext/v1/campaigns \
-H "Authorization: Bearer $SKYFOLLOWING_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Client launch",
"type": "keyword",
"keywords": [
"indie games",
"gamedev"
],
"dailyFollowLimit": 30,
"minAiScore": 70
}'201
{
"data": {
"id": "8c2f4d1e-…",
"name": "Client launch",
"status": "draft",
"type": "keyword",
"keywords": ["indie games", "gamedev"],
"minAiScore": 70,
"dailyFollowLimit": 30,
"followBackWaitDays": 10,
"activeHoursStart": 9,
"activeHoursEnd": 21,
"createdAt": "2026-07-01T09:12:00.000Z"
}
}The builder writes requests; it never sends them. Responses shown are representative examples.
Endpoints
/api/ext/v1/campaignsList campaigns
Every campaign in the workspace, each with a summary of the connected account it runs on.
curl https://skyfollowing.com/api/ext/v1/campaigns \ -H "Authorization: Bearer $SKYFOLLOWING_API_KEY"
{
"data": [
{
"id": "8c2f4d1e-…",
"name": "Design niche",
"status": "active",
"type": "keyword",
"keywords": ["design systems"],
"seedHandles": [],
"dailyFollowLimit": 30,
"createdAt": "2026-07-01T09:12:00.000Z",
"account": {
"id": "b7a1…",
"handle": "studio.bsky.social",
"avatarUrl": null,
"status": "active",
"lastError": null
}
}
]
}/api/ext/v1/campaignsCreate a campaign
Creates a draft by default; pass startActive to launch immediately. The request is validated against your plan: dailyFollowLimit beyond your plan cap or a campaign count past your plan limit returns 403.
| Field | Type | Required / default | Notes |
|---|---|---|---|
| name | string | required | 1-80 characters. |
| blueskyAccountId | uuid | required | A connected account in your workspace. Invalid accounts are rejected. |
| type | enum | keyword | keyword · lookalike · trending · competitor. |
| keywords | string[] | [] | Required (non-empty) for keyword and trending campaigns. |
| negativeKeywords | string[] | [] | Candidates matching these are excluded before scoring. |
| seedHandles | string[] | [] | Required (non-empty) for lookalike and competitor campaigns. Leading @ is stripped. |
| minFollowers | int ≥ 0 | 0 | Candidate follower-count floor. |
| maxFollowers | int ≥ 0 | 100000 | Must be greater than or equal to minFollowers. |
| dailyFollowLimit | int 1-250 | 30 | Rejected with 403 if it exceeds your plan's daily cap. |
| followBackWaitDays | int 1-30 | 10 | Cleanup wait window before non-followers are released. |
| unfollowDailyLimit | int 0-100 | 20 | Daily ceiling on cleanup unfollows. |
| activeHoursStart | int 0-23 | 9 | Window start hour in the campaign timezone. |
| activeHoursEnd | int 0-24 | 21 | Window end; may wrap midnight. Equal start and end means around the clock. |
| timezone | string | America/Los_Angeles | IANA timezone for the active-hours window. |
| aiScoringEnabled | boolean | true | Score every candidate 0-100 before queueing. |
| minAiScore | int 0-100 | 60 | Candidates below the bar never enter the queue. |
| botFilterEnabled | boolean | true | Deterministic bot heuristics ahead of AI scoring. |
| startActive | boolean | false | true launches immediately; false creates a draft. |
curl -X POST https://skyfollowing.com/api/ext/v1/campaigns \
-H "Authorization: Bearer $SKYFOLLOWING_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Client launch",
"blueskyAccountId": "b7a1…",
"type": "keyword",
"keywords": ["specialty coffee", "home espresso"],
"dailyFollowLimit": 30,
"minAiScore": 70,
"startActive": true
}'{
"data": {
"id": "8c2f4d1e-…",
"name": "Client launch",
"status": "active",
"type": "keyword",
"keywords": ["specialty coffee", "home espresso"],
"minAiScore": 70,
"dailyFollowLimit": 30,
"followBackWaitDays": 10,
"activeHoursStart": 9,
"activeHoursEnd": 21,
"timezone": "America/Los_Angeles",
"createdAt": "2026-07-01T09:12:00.000Z"
}
}/api/ext/v1/campaigns/{id}Get a campaign
The full campaign record, including every quality and pacing setting. Returns 404 for campaigns outside your workspace.
curl https://skyfollowing.com/api/ext/v1/campaigns/8c2f4d1e-… \ -H "Authorization: Bearer $SKYFOLLOWING_API_KEY"
{
"data": {
"id": "8c2f4d1e-…",
"name": "Client launch",
"status": "active",
"type": "keyword",
"aiScoringEnabled": true,
"minAiScore": 70,
"botFilterEnabled": true,
"minFollowers": 0,
"maxFollowers": 100000,
"dailyFollowLimit": 30,
"unfollowDailyLimit": 20,
"followBackWaitDays": 10,
"activeHoursStart": 9,
"activeHoursEnd": 21,
"timezone": "America/Los_Angeles"
}
}/api/ext/v1/campaigns/{id}Update a campaign
Partial update: send only the fields you are changing, with at least one field present. This is also how you pause and resume programmatically.
| Field | Type | Required / default | Notes |
|---|---|---|---|
| status | enum | optional | draft · active · paused · completed · error. Pausing stops new actions immediately. |
| name, keywords, negativeKeywords, seedHandles | as in create | optional | Targeting fields; changes apply from the next research run. |
| minFollowers, maxFollowers, minAiScore, aiScoringEnabled, botFilterEnabled | as in create | optional | Quality bar; applies to candidates from the next action onward. |
| dailyFollowLimit, unfollowDailyLimit, followBackWaitDays, activeHoursStart, activeHoursEnd, timezone | as in create | optional | Pacing; same bounds as create. At least one field is required overall. |
curl -X PATCH https://skyfollowing.com/api/ext/v1/campaigns/8c2f4d1e-… \
-H "Authorization: Bearer $SKYFOLLOWING_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "status": "paused" }'{
"data": {
"id": "8c2f4d1e-…",
"status": "paused",
"updatedAt": "2026-07-01T10:03:41.000Z"
}
}/api/ext/v1/campaigns/{id}/statsCampaign stats
Cohort attribution for one campaign. followBackRate is followedBack divided by followed, rounded to a whole percentage.
curl https://skyfollowing.com/api/ext/v1/campaigns/8c2f4d1e-…/stats \ -H "Authorization: Bearer $SKYFOLLOWING_API_KEY"
{
"data": {
"total": 475,
"followed": 286,
"followedBack": 81,
"unfollowed": 63,
"queued": 126,
"followBackRate": 28
}
}/api/ext/v1/save-profileQueue a profile
Resolves a handle against Bluesky and queues the profile into a campaign, deduplicating automatically. Built for browser extensions and internal tools; a profile_saved event is recorded and delivered to webhooks.
| Field | Type | Required / default | Notes |
|---|---|---|---|
| campaignId | uuid | required | The campaign whose queue receives the profile. |
| handle | string | required | Accepts ada.bsky.social, @ada.bsky.social, or a full bsky.app profile URL. |
curl -X POST https://skyfollowing.com/api/ext/v1/save-profile \
-H "Authorization: Bearer $SKYFOLLOWING_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"campaignId": "8c2f4d1e-…",
"handle": "@ada.bsky.social"
}'{
"data": {
"campaignId": "8c2f4d1e-…",
"did": "did:plc:ab12cd34…",
"handle": "ada.bsky.social",
"queued": true
}
}Signed webhooks push follow-backs, pauses, and every other event to your endpoint, in order and with retries.