vlayervlayer docs
API reference

Challenge data

Read and refresh the comments, likers and post metrics on a challenge post, and administer its attempts.

List stored comments

Get paginated comments stored for a challenge post.

GET
/admin/challenges/{challengeId}/comments

Authorization

AuthorizationRequiredBearer <token>

API token. Send it as Authorization: Bearer <API_TOKEN>.

In: header

Path Parameters

challengeIdRequiredinteger

Query Parameters

pageinteger
Default: 1
limitinteger
Default: 20
curl -X GET "//admin/challenges/{challengeId}/comments" \
  -H "Authorization: Bearer <token>"

Comment list

{
  "data": [
    {
      "id": 1,
      "platformCommentId": "17890012345678",
      "username": "creator_jane",
      "commentText": "Love this product!",
      "commentedAt": "2019-08-24T14:15:22Z",
      "createdAt": "2019-08-24T14:15:22Z"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 150
  }
}

Get latest post metrics

Returns the most recent metrics snapshot for the challenge post (likes, comments, shares, views).

data is the last successful scrape, so its fetchedAt alone cannot tell you whether the numbers are current — a challenge whose refreshes have been failing for days keeps returning its last good snapshot. Read lastRefresh for that: it records the most recent attempt and its outcome (success, rate_limited, error). If lastRefresh.attemptedAt is much newer than data.fetchedAt, the numbers are stale and something is failing.

lastRefresh is null if the challenge has never been refreshed since this tracking was introduced.

GET
/admin/challenges/{challengeId}/metrics

Authorization

AuthorizationRequiredBearer <token>

API token. Send it as Authorization: Bearer <API_TOKEN>.

In: header

Path Parameters

challengeIdRequiredinteger
curl -X GET "//admin/challenges/{challengeId}/metrics" \
  -H "Authorization: Bearer <token>"

Latest metrics plus the outcome of the last refresh attempt

{
  "data": {
    "likeCount": 5432,
    "commentCount": 312,
    "shareCount": 89,
    "viewCount": 125000,
    "thumbnailUrl": "https://scontent.cdninstagram.com/v/t51.82787-15/792667282.jpg?stp=dst-jpg&oh=00_AQ&oe=6A9F2C99",
    "fetchedAt": "2019-08-24T14:15:22Z"
  },
  "lastRefresh": {
    "status": "success",
    "errorCode": "SCRAPING_RATE_LIMITED",
    "attemptedAt": "2019-08-24T14:15:22Z",
    "durationMs": 27134
  }
}

Force-refresh post metrics

Fetches fresh metrics (Instagram post info or TikTok video info) and stores a new snapshot. Use this to get up-to-date like/comment/share/view counts.

Two success shapes. 200 means the snapshot was taken and is in the body. 202 means the scraping provider is rate limiting us, so the refresh has been queued and will run in the background — poll GET /admin/challenges/{challengeId}/metrics after the number of seconds given in retryAfterSeconds (also sent as the Retry-After header).

Only one background refresh exists per challenge at a time. status distinguishes the two cases: queued means this call created it, already_scheduled means one was already pending or running and nothing new was enqueued. Calling repeatedly is therefore safe and cheap — it will not stack jobs.

Cooldown. Each challenge can be refreshed at most once every 5 minutes; sooner returns 429 RATE_LIMITED with a Retry-After. The cooldown is per challenge, not per caller, so sweeping many challenges is unaffected — only re-refreshing the same one is. The cooldown is keyed on the challenge id itself, so /1, /01 and /1.0 share one budget rather than three. An id that is not a positive integer is rejected with 422 VALIDATION_ERROR before any provider call.

If the post cannot be read at all, the reason is propagated — see the ScrapingError schema.

Age-restricted Instagram posts are read through a fallback provider, so they normally return 200 with the counts. 422 POST_AGE_RESTRICTED now means both providers failed, and note that such a snapshot carries no viewCount — the fallback reports no play count even for a reel.

POST
/admin/challenges/{challengeId}/metrics/refresh

Authorization

AuthorizationRequiredBearer <token>

API token. Send it as Authorization: Bearer <API_TOKEN>.

In: header

Path Parameters

challengeIdRequiredinteger
curl -X POST "//admin/challenges/{challengeId}/metrics/refresh" \
  -H "Authorization: Bearer <token>"

Freshly fetched metrics

{
  "data": {
    "likeCount": 5432,
    "commentCount": 312,
    "shareCount": 89,
    "viewCount": 125000,
    "thumbnailUrl": "https://scontent.cdninstagram.com/v/t51.82787-15/792667282.jpg?stp=dst-jpg&oh=00_AQ&oe=6A9F2C99",
    "fetchedAt": "2019-08-24T14:15:22Z"
  }
}

Force-refresh comments

Reads the post and stores what it finds, newest first, plus Instagram threaded replies on a best-effort basis. New comments are inserted; existing ones are matched on platform comment ID and left unchanged. Worth calling once when launching a campaign on a post that already has comments.

One call does not always reach the end of a large post. The call walks the newest comments, then works backwards through the history from where the last call stopped, and gives up after about four minutes. Call it again to carry on.

totalFetched is the number of comments stored for the post, not the number this call added. sweepStatus is swept when this call walked the comments, or in_progress when another read of the same post was already running, so this call left the walk to it — on Instagram the reply pass still runs either way. historyComplete reports whether the stored comments are as complete as they will get; false means there is older history still to fetch.

Once 6,000 comments are stored for a post, the walk back through older history stops. Newer comments carry on being stored.

If the post cannot be read, the reason is propagated — see the ScrapingError schema.

POST
/admin/challenges/{challengeId}/comments/refresh

Authorization

AuthorizationRequiredBearer <token>

API token. Send it as Authorization: Bearer <API_TOKEN>.

In: header

Path Parameters

challengeIdRequiredinteger
curl -X POST "//admin/challenges/{challengeId}/comments/refresh" \
  -H "Authorization: Bearer <token>"

Refresh result

{
  "data": {
    "totalFetched": 347,
    "sweepStatus": "swept",
    "historyComplete": true
  }
}

Create attempt for a user (admin)

Create an attempt on behalf of a specific creator. Caller must be admin. Identify the creator by either userId (internal integer id) or externalId (customer-side identifier stored on users.externalId).

POST
/admin/challenges/{challengeId}/attempts

Authorization

AuthorizationRequiredBearer <token>

API token. Send it as Authorization: Bearer <API_TOKEN>.

In: header

Request Body

application/jsonRequired
userIdinteger

Internal user id. Mutually exclusive with externalId — provide one.

externalIdstring

Customer-side identifier stored on users.externalId. Mutually exclusive with userId — provide one.

Path Parameters

challengeIdRequiredinteger
curl -X POST "//admin/challenges/{challengeId}/attempts" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "userId": 42,
    "externalId": "cust_abc123"
  }'

Attempt created

{
  "data": {
    "id": 1,
    "userId": 42,
    "challengeId": 1,
    "state": "created",
    "retryCount": 0,
    "maxRetries": 10,
    "lastVerificationAt": "2019-08-24T14:15:22Z",
    "verificationDeadline": "2019-08-24T14:15:22Z",
    "stateHistory": [
      {
        "state": "string",
        "at": "2019-08-24T14:15:22Z",
        "reason": "string"
      }
    ],
    "metadata": {},
    "createdAt": "2019-08-24T14:15:22Z",
    "updatedAt": "2019-08-24T14:15:22Z"
  }
}

Get attempt by ID

GET
/admin/challenges/{challengeId}/attempts/{attemptId}

Authorization

AuthorizationRequiredBearer <token>

API token. Send it as Authorization: Bearer <API_TOKEN>.

In: header

Path Parameters

challengeIdRequiredinteger
attemptIdRequiredinteger
curl -X GET "//admin/challenges/{challengeId}/attempts/{attemptId}" \
  -H "Authorization: Bearer <token>"

Attempt details

{
  "data": {
    "id": 1,
    "state": "verified",
    "retryCount": 0,
    "maxRetries": 10,
    "matchedComment": {
      "commentText": "Love this product! #SummerVibes",
      "commentedAt": "2019-08-24T14:15:22Z"
    },
    "pointsAwarded": 100,
    "createdAt": "2019-08-24T14:15:22Z",
    "updatedAt": "2019-08-24T14:15:22Z"
  }
}

Progress attempt for a user (admin)

Advance an attempt's state on behalf of the attempt's owning creator. Caller must be admin. The owning user is resolved from the attempt row — no userId/externalId needed in the body.

PATCH
/admin/challenges/{challengeId}/attempts/{attemptId}

Authorization

AuthorizationRequiredBearer <token>

API token. Send it as Authorization: Bearer <API_TOKEN>.

In: header

Request Body

application/jsonRequired
actionRequiredstring
Value in: "target_opened" | "claim_completion"

Path Parameters

challengeIdRequiredinteger
attemptIdRequiredinteger
curl -X PATCH "//admin/challenges/{challengeId}/attempts/{attemptId}" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "target_opened"
  }'

Attempt updated

{
  "data": {
    "id": 1,
    "state": "target_opened"
  }
}

Force-verify attempt

Moves an attempt straight to verified, schedules the reward, and sends creator.verified followed by reward.issued.

Every state except verified and rewarded can be force-verified, including failed_terminal — an attempt that has exhausted its retries is the main reason to reach for this route.

POST
/admin/challenges/{challengeId}/attempts/{attemptId}/force-verify

Authorization

AuthorizationRequiredBearer <token>

API token. Send it as Authorization: Bearer <API_TOKEN>.

In: header

Path Parameters

challengeIdRequiredinteger
attemptIdRequiredinteger
curl -X POST "//admin/challenges/{challengeId}/attempts/{attemptId}/force-verify" \
  -H "Authorization: Bearer <token>"

Attempt force-verified

{
  "data": {
    "id": 0,
    "state": "verified",
    "previousState": "verification_pending"
  }
}

Redeliver outbound webhooks for attempt

Re-enqueues outbound brand webhooks (creator.verified, reward.issued, verification.failed) for an attempt. Use after a receiver outage to replay deliveries that exhausted their pg-boss retries. If events is omitted, every event eligible for the attempt's current state is redelivered (rewarded → both verified + reward; verified → verified only; failed_* → verification.failed). Caller must be admin.

POST
/admin/challenges/{challengeId}/attempts/{attemptId}/redeliver-webhooks

Authorization

AuthorizationRequiredBearer <token>

API token. Send it as Authorization: Bearer <API_TOKEN>.

In: header

Request Body

application/jsonOptional
eventsarray<string>

Path Parameters

challengeIdRequiredinteger
attemptIdRequiredinteger
curl -X POST "//admin/challenges/{challengeId}/attempts/{attemptId}/redeliver-webhooks" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "events": [
      "creator.verified"
    ]
  }'

Redelivery enqueued

{
  "data": {
    "attemptId": 0,
    "redelivered": [
      "string"
    ]
  }
}

Get attempt by social handle

Look up a creator's attempt for a challenge by their social media handle (e.g. creator_jane or @creator_jane). Returns null if no attempt found.

GET
/admin/challenges/{challengeId}/attempts/by-handle/{handle}

Authorization

AuthorizationRequiredBearer <token>

API token. Send it as Authorization: Bearer <API_TOKEN>.

In: header

Path Parameters

challengeIdRequiredinteger
handleRequiredstring
curl -X GET "//admin/challenges/{challengeId}/attempts/by-handle/creator_jane" \
  -H "Authorization: Bearer <token>"

Attempt for this handle (or null)

{
  "data": {
    "id": 1,
    "state": "verified",
    "retryCount": 0,
    "maxRetries": 10,
    "matchedComment": {
      "commentText": "Love this product! #SummerVibes",
      "commentedAt": "2019-08-24T14:15:22Z"
    },
    "pointsAwarded": 100,
    "createdAt": "2019-08-24T14:15:22Z",
    "updatedAt": "2019-08-24T14:15:22Z"
  }
}

Fetch likers for a challenge's Instagram post

Starts an asynchronous fetch of the likers of the Instagram post this challenge already references — the post is derived from the challenge, so no shortcode/URL is supplied. Returns 202 immediately; poll the status endpoint for progress.

Coverage caveat: this uses a public/no-cookie scraper, so the number of fetched liker profiles may be lower than Instagram's visible like count. Only supported for Instagram challenges.

POST
/admin/challenges/{challengeId}/likers/refresh

Authorization

AuthorizationRequiredBearer <token>

API token. Send it as Authorization: Bearer <API_TOKEN>.

In: header

Path Parameters

challengeIdRequiredinteger

Query Parameters

maxCountinteger

Max likers to fetch. Defaults to 1000.

Default: 1000Minimum: 1Maximum: 5000
curl -X POST "//admin/challenges/{challengeId}/likers/refresh" \
  -H "Authorization: Bearer <token>"

Fetch accepted and started

{
  "data": {
    "challengeId": 1,
    "status": "succeeded",
    "fetchedCount": 428,
    "totalLikes": 432,
    "errorMessage": "string",
    "startedAt": "2019-08-24T14:15:22Z",
    "finishedAt": "2019-08-24T14:15:22Z",
    "requestedAt": "2019-08-24T14:15:22Z",
    "warning": "string"
  }
}

Status of the latest likers fetch for a challenge

GET
/admin/challenges/{challengeId}/likers/status

Authorization

AuthorizationRequiredBearer <token>

API token. Send it as Authorization: Bearer <API_TOKEN>.

In: header

Path Parameters

challengeIdRequiredinteger
curl -X GET "//admin/challenges/{challengeId}/likers/status" \
  -H "Authorization: Bearer <token>"

Latest fetch status, or null if none has been requested

{
  "data": {
    "challengeId": 1,
    "status": "succeeded",
    "fetchedCount": 428,
    "totalLikes": 432,
    "errorMessage": "string",
    "startedAt": "2019-08-24T14:15:22Z",
    "finishedAt": "2019-08-24T14:15:22Z",
    "requestedAt": "2019-08-24T14:15:22Z",
    "warning": "string"
  }
}

List stored likers for a challenge (paginated)

Returns likers from the most recent completed fetch for the challenge. Empty if no fetch has succeeded yet.

GET
/admin/challenges/{challengeId}/likers

Authorization

AuthorizationRequiredBearer <token>

API token. Send it as Authorization: Bearer <API_TOKEN>.

In: header

Path Parameters

challengeIdRequiredinteger

Query Parameters

pageinteger
Default: 1
limitinteger
Default: 20Maximum: 100
curl -X GET "//admin/challenges/{challengeId}/likers" \
  -H "Authorization: Bearer <token>"

Liker rows

{
  "data": [
    {
      "id": 1,
      "username": "jane_doe",
      "fullName": "Jane Doe",
      "igUserId": "1784500123",
      "isPrivate": true,
      "isVerified": true,
      "profilePicUrl": "string",
      "likedPost": true,
      "postUrl": "string",
      "totalLikes": 0
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 150
  },
  "warning": "string"
}

Last updated on