vlayervlayer docs
Engagement verification

Comments and metrics on a challenge

Refresh and read the comments and post metrics stored for a challenge post, as engagement verification uses them.

A challenge keeps its own copy of the post's comments and metrics. A refresh route reads the platform and stores what it finds; the GET routes return the stored copy and never touch the platform. Nothing refreshes on a schedule.

For a one-off read of any public post, without a campaign, use the Comments and Post metrics lookups instead.

List stored comments

GET/admin/challenges/{challengeId}/comments?page=1&limit=20
curl "{API_BASE_URL}/admin/challenges/1/comments?page=1&limit=50" \
  -H "Authorization: Bearer <API_TOKEN>"
{
  "data": [
    {
      "id": 1,
      "platformCommentId": "17890012345678",
      "username": "creator_jane",
      "commentText": "Love this product! #SummerVibes",
      "commentedAt": "2026-06-02T14:21:07.000Z",
      "createdAt": "2026-06-02T15:00:11.000Z"
    }
  ],
  "pagination": { "page": 1, "limit": 50, "total": 347 }
}
FieldTypeMeaning
idintegerStored row id
platformCommentIdstringThe platform's comment id. Used for de-duplication
usernamestringCommenter's handle
commentTextstring
commentedAtdate-timeWhen the comment was posted
createdAtdate-timeWhen the row was stored

Refresh comments

POST/admin/challenges/{challengeId}/comments/refreshData lookup

Reads the post and stores what it finds, newest first, and for Instagram threaded replies on a best-effort basis. New comments are inserted; existing ones are matched on platformCommentId and left unchanged.

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. historyComplete tells you whether there is more to come; call it again to carry on.

curl -X POST {API_BASE_URL}/admin/challenges/1/comments/refresh \
  -H "Authorization: Bearer <API_TOKEN>"
{ "data": { "totalFetched": 347, "sweepStatus": "swept", "historyComplete": true } }
FieldMeaning
totalFetchedComments now stored for the post, not the number this call added
sweepStatusswept when this call walked the comments. 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
historyCompletetrue when the stored comments are as complete as they will get. false when there is older history still to fetch; call again to continue

Once 6,000 comments are stored for a post, the walk back through older history stops and historyComplete reports true. Newer comments carry on being stored, so the total keeps growing past 6,000.

Worth calling once when you create a campaign on a post that already has comments. While an attempt is being verified, the API reads new comments itself.

Comment refresh errors

When the post cannot be read, the refresh route returns one of the data lookup errors. 422 and 404 are permanent for that post; 429, 502, and 503 are worth retrying.

Latest metrics snapshot

GET/admin/challenges/{challengeId}/metrics
curl {API_BASE_URL}/admin/challenges/1/metrics \
  -H "Authorization: Bearer <API_TOKEN>"
{
  "data": {
    "likeCount": 5432,
    "commentCount": 312,
    "shareCount": 89,
    "viewCount": 125000,
    "thumbnailUrl": "https://...",
    "fetchedAt": "2026-06-02T15:00:11.000Z"
  },
  "lastRefresh": {
    "status": "success",
    "errorCode": null,
    "attemptedAt": "2026-06-02T15:00:11.000Z",
    "durationMs": 2713
  }
}

data is the last successful snapshot, or null when none has been taken yet. On its own, fetchedAt cannot tell you whether the numbers are current: a challenge whose refreshes keep failing goes on returning its last good snapshot. lastRefresh records the most recent refresh and how it ended. If lastRefresh.attemptedAt is much newer than data.fetchedAt, the numbers are stale and refreshes are failing.

FieldTypeNotes
likeCountinteger or nullnull when the author hides the like count
commentCountinteger
shareCountinteger or nullAlways null on Instagram
viewCountinteger or nullReels and TikTok videos; null for image posts
thumbnailUrlstring or nullThe post's cover image. A signed URL that expires within days; refresh to get a working one
fetchedAtdate-timeWhen this snapshot was taken
lastRefresh.statusstringsuccess, rate_limited or error
lastRefresh.errorCodestring or nullThe data lookup error when the refresh failed
lastRefresh.attemptedAtdate-timeWhen the most recent refresh ran. lastRefresh is null if the challenge has never been refreshed
lastRefresh.durationMsintegerHow long that refresh took

Refresh metrics

POST/admin/challenges/{challengeId}/metrics/refreshData lookup
curl -X POST {API_BASE_URL}/admin/challenges/1/metrics/refresh \
  -H "Authorization: Bearer <API_TOKEN>"
StatusMeaning
200The snapshot was taken. data is the new PostMetrics object
202Reads are being throttled upstream, so the refresh was queued and runs in the background. Read the latest snapshot again after retryAfterSeconds
429 RATE_LIMITEDThis challenge was refreshed less than 5 minutes ago. Wait for the Retry-After header
{ "data": { "challengeId": 1, "status": "queued", "retryAfterSeconds": 180 } }

In a 202, status is queued when this call scheduled the refresh and already_scheduled when one was already pending. Only one background refresh exists per challenge, so calling again is safe and does not stack work. The 5-minute cooldown is per challenge, not per caller: refreshing many challenges in a row is unaffected.

Metrics refresh errors

The refresh route returns a data lookup error when the post cannot be read. An age-restricted Instagram post is usually still read and returns 200, with viewCount as null. 422 POST_AGE_RESTRICTED means it could not be read at all.

Last updated on