vlayer logovlayer

Verify engagement

Track a creator's attempt through the state machine, trigger verification of a comment or like, and use the admin routes to force, look up, or batch-verify.

An attempt is one creator's run at one challenge. You create it, advance it as the creator acts, and the API verifies the comment or like in the background. The outcome reaches you through webhooks.

States

created → target_opened → verification_pending → verified → rewarded

                     failed_retryable → (retry) → verification_pending

                             failed_terminal
StateMeaning
createdAttempt exists; the creator has not opened the post yet
target_openedThe creator viewed the post
verification_pendingThe creator claimed completion; verification is running
verifiedA matching comment or like was found. creator.verified sent
rewardedPoints issued. reward.issued sent
failed_retryableNo match yet; the attempt can be claimed again. verification.failed with reason: "retryable"
failed_terminalRetries exhausted or the post cannot be read. verification.failed with reason: "terminal"

awaiting_claim also appears in the state enum. Treat it like target_opened: claim_completion is accepted from it.

Actions

PATCH …/attempts/{attemptId} takes one action:

actionAllowed fromMoves to
target_openedcreated, target_openedtarget_opened
claim_completiontarget_opened, awaiting_claim, failed_retryableverification_pending

Any other combination returns 409. In particular, claim_completion straight from created is rejected: send target_opened first.

Run an attempt for a creator

The admin routes act on behalf of a registered creator, identified by userId or externalId (exactly one).

1. Create the attempt

POST /admin/challenges/{challengeId}/attempts

curl -X POST {API_BASE_URL}/admin/challenges/1/attempts \
  -H "Authorization: Bearer <API_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{ "externalId": "cust_abc123" }'
{
  "data": {
    "id": 39,
    "userId": 42,
    "challengeId": 1,
    "state": "created",
    "retryCount": 0,
    "maxRetries": 10,
    "lastVerificationAt": null,
    "verificationDeadline": null,
    "stateHistory": [],
    "metadata": null,
    "createdAt": "2026-06-02T15:00:11.000Z",
    "updatedAt": "2026-06-02T15:00:11.000Z"
  }
}
StatuserrorMeaning
201Created
400Neither userId nor externalId given
404Creator or challenge not found
409ACTIVE_ATTEMPT_EXISTSThe creator already has an open attempt on this challenge
409ALREADY_REWARDEDThe creator was already rewarded for this challenge

2. Mark the post opened

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

curl -X PATCH {API_BASE_URL}/admin/challenges/1/attempts/39 \
  -H "Authorization: Bearer <API_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{ "action": "target_opened" }'
{ "data": { "id": 39, "state": "target_opened" } }

3. Claim completion

Same route with { "action": "claim_completion" }. The attempt moves to verification_pending and verification starts. 409 STATE_CONFLICT or 409 VERIFICATION_IN_PROGRESS means the state changed under you; re-read the attempt.

4. Wait for the result

Verification is asynchronous. Subscribe to creator.verified, reward.issued, and verification.failed on your webhook, or poll:

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

{
  "data": {
    "id": 39,
    "state": "verified",
    "retryCount": 0,
    "maxRetries": 10,
    "matchedComment": { "commentText": "Love this product! #SummerVibes", "commentedAt": "2026-06-02T15:04:40.000Z" },
    "pointsAwarded": null,
    "createdAt": "2026-06-02T15:00:11.000Z",
    "updatedAt": "2026-06-02T15:05:02.000Z"
  }
}

A failed_retryable attempt accepts claim_completion again, up to maxRetries.

Creator-side routes

POST /challenges/{challengeId}/attempts and PATCH /challenges/{challengeId}/attempts/{attemptId} do the same thing for the creator's own token, resolving the creator from the token instead of the body. Use them when creators call your API directly.

What counts as a match

Comment challenges

The attempt verifies when the post has a comment whose username equals the creator's linked handle for the challenge's platform and that was posted after the attempt was created. The matched text is returned as matchedComment and in the creator.verified webhook as commentText.

Like challenges

Create the campaign with challenge.action: "like" (Instagram only). On claim_completion, the API ensures a fresh likers fetch exists for the post, waits for it to finish, and verifies when the creator's linked handle appears in the liker set. The comparison is case-insensitive. If the fetch does not finish in time or fails, the attempt fails with the usual retryable or terminal semantics.

Because the public liker fetch can return fewer profiles than Instagram's visible like count, a creator who did like the post may still not verify. Use force-verify for those cases after checking manually.

Admin tools

Look up by handle

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

Returns the creator's attempt for the challenge (Attempt shape as above) or null. The handle may include @.

Force-verify

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

Moves any attempt that is not yet verified, rewarded, or failed_terminal straight to verified, schedules the reward, and sends creator.verified followed by reward.issued.

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

409 if the attempt is already verified or rewarded.

Batch verify and reward

POST /admin/campaigns/{id}/verify-and-reward

For every registered creator whose handle appears in the stored comments of the campaign's post and who has no attempt yet: creates an attempt in verified, schedules the reward, and sends the webhooks. Refresh comments first so the stored set is current.

{ "data": { "matched": 15, "verified": 14, "rewarded": 14 } }

Redeliver webhooks

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

Re-sends the webhooks for an attempt after a receiver outage. Optional body { "events": ["creator.verified"] }; omit it to resend every event the attempt's current state qualifies for.