SkyFollowing
API reference

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.

Base URL · /api/ext/v1Auth · Bearer keyRate limit · 100 req/minPlan · Agency

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.

Every request
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.

StatusWhen
400Validation failed; the message names the first invalid field
401Missing bearer token, or the key is invalid or revoked
403Plan does not include API access, or a plan limit was exceeded
404Campaign or Bluesky profile not found in your workspace
429More than 100 requests in a minute on this key
Interactive
Build a request

Pick an endpoint, fill the fields, copy the curl.

Request
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
}'
Example response
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

GET/api/ext/v1/campaigns

List campaigns

Every campaign in the workspace, each with a summary of the connected account it runs on.

Request
curl https://skyfollowing.com/api/ext/v1/campaigns \
  -H "Authorization: Bearer $SKYFOLLOWING_API_KEY"
Response 200
{
  "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
      }
    }
  ]
}
POST/api/ext/v1/campaigns

Create 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.

Body
FieldTypeRequired / defaultNotes
namestringrequired1-80 characters.
blueskyAccountIduuidrequiredA connected account in your workspace. Invalid accounts are rejected.
typeenumkeywordkeyword · lookalike · trending · competitor.
keywordsstring[][]Required (non-empty) for keyword and trending campaigns.
negativeKeywordsstring[][]Candidates matching these are excluded before scoring.
seedHandlesstring[][]Required (non-empty) for lookalike and competitor campaigns. Leading @ is stripped.
minFollowersint ≥ 00Candidate follower-count floor.
maxFollowersint ≥ 0100000Must be greater than or equal to minFollowers.
dailyFollowLimitint 1-25030Rejected with 403 if it exceeds your plan's daily cap.
followBackWaitDaysint 1-3010Cleanup wait window before non-followers are released.
unfollowDailyLimitint 0-10020Daily ceiling on cleanup unfollows.
activeHoursStartint 0-239Window start hour in the campaign timezone.
activeHoursEndint 0-2421Window end; may wrap midnight. Equal start and end means around the clock.
timezonestringAmerica/Los_AngelesIANA timezone for the active-hours window.
aiScoringEnabledbooleantrueScore every candidate 0-100 before queueing.
minAiScoreint 0-10060Candidates below the bar never enter the queue.
botFilterEnabledbooleantrueDeterministic bot heuristics ahead of AI scoring.
startActivebooleanfalsetrue launches immediately; false creates a draft.
Request
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
  }'
Response 201
{
  "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"
  }
}
GET/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.

Request
curl https://skyfollowing.com/api/ext/v1/campaigns/8c2f4d1e-… \
  -H "Authorization: Bearer $SKYFOLLOWING_API_KEY"
Response 200
{
  "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"
  }
}
PATCH/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.

Body (all optional)
FieldTypeRequired / defaultNotes
statusenumoptionaldraft · active · paused · completed · error. Pausing stops new actions immediately.
name, keywords, negativeKeywords, seedHandlesas in createoptionalTargeting fields; changes apply from the next research run.
minFollowers, maxFollowers, minAiScore, aiScoringEnabled, botFilterEnabledas in createoptionalQuality bar; applies to candidates from the next action onward.
dailyFollowLimit, unfollowDailyLimit, followBackWaitDays, activeHoursStart, activeHoursEnd, timezoneas in createoptionalPacing; same bounds as create. At least one field is required overall.
Request
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" }'
Response 200
{
  "data": {
    "id": "8c2f4d1e-…",
    "status": "paused",
    "updatedAt": "2026-07-01T10:03:41.000Z"
  }
}
GET/api/ext/v1/campaigns/{id}/stats

Campaign stats

Cohort attribution for one campaign. followBackRate is followedBack divided by followed, rounded to a whole percentage.

Request
curl https://skyfollowing.com/api/ext/v1/campaigns/8c2f4d1e-…/stats \
  -H "Authorization: Bearer $SKYFOLLOWING_API_KEY"
Response 200
{
  "data": {
    "total": 475,
    "followed": 286,
    "followedBack": 81,
    "unfollowed": 63,
    "queued": 126,
    "followBackRate": 28
  }
}
POST/api/ext/v1/save-profile

Queue 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.

Body
FieldTypeRequired / defaultNotes
campaignIduuidrequiredThe campaign whose queue receives the profile.
handlestringrequiredAccepts ada.bsky.social, @ada.bsky.social, or a full bsky.app profile URL.
Request
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"
  }'
Response 200
{
  "data": {
    "campaignId": "8c2f4d1e-…",
    "did": "did:plc:ab12cd34…",
    "handle": "ada.bsky.social",
    "queued": true
  }
}
Going the other direction?

Signed webhooks push follow-backs, pauses, and every other event to your endpoint, in order and with retries.

Webhook docs