Engagement verification
Verify that a creator commented on or liked a post, and use the admin routes to force, look up or batch-verify attempts.
Engagement verification is available on request. Book a call (opens in a new tab) to have it enabled; the data lookups work without it. Campaigns, challenges, registered creators and Instagram likers belong to engagement verification.
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.
Typical integration
- Register a webhook for your brand and store the
whsec_*secret. - Create a campaign from the post URL. Keep the returned
challengeId. - Register each creator with your
externalIdand their handle. - When a creator takes part, create an attempt and advance it to
claim_completion. - Handle
creator.verifiedandreward.issuedon your webhook endpoint.
To verify in batch instead of steps 4 and 5, refresh comments and call verify-and-reward on the campaign.
States
created → target_opened → verification_pending → verified → rewarded
↓
failed_retryable → (retry) → verification_pending
↓
failed_terminal| State | Meaning |
|---|---|
created | Attempt exists; the creator has not opened the post yet |
target_opened | The creator viewed the post |
verification_pending | The creator claimed completion; verification is running |
verified | A matching comment or like was found. creator.verified sent |
rewarded | Points issued. reward.issued sent |
failed_retryable | No match yet; the attempt can be claimed again. verification.failed with reason: "retryable" |
failed_terminal | Retries 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:
action | Allowed from | Moves to |
|---|---|---|
target_opened | created, target_opened | target_opened |
claim_completion | target_opened, awaiting_claim, failed_retryable | verification_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
/admin/challenges/{challengeId}/attemptscurl -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"
}
}| Status | error | Meaning |
|---|---|---|
201 | Created | |
422 | VALIDATION_ERROR | Neither userId nor externalId given; or the campaign is inactive or ended; or the creator has no linked account for the platform |
404 | Creator or challenge not found | |
409 | ACTIVE_ATTEMPT_EXISTS | The creator already has an open attempt on this challenge |
409 | ALREADY_REWARDED | The creator was already rewarded for this challenge |
2. Mark the post opened
/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:
/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
/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
/admin/challenges/{challengeId}/attempts/{attemptId}/force-verifyMoves any attempt that has not already reached verified or rewarded straight to verified, schedules the reward, and sends creator.verified followed by reward.issued. An attempt that gave up at failed_terminal can still be force-verified — that is the main reason to use this route.
{ "data": { "id": 39, "state": "verified", "previousState": "verification_pending" } }409 ALREADY_VERIFIED or 409 ALREADY_REWARDED if the attempt is already in that state, and 409 STATE_CONFLICT if something moves it while the call is in flight.
Batch verify and reward
/admin/campaigns/{id}/verify-and-rewardFor 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
/admin/challenges/{challengeId}/attempts/{attemptId}/redeliver-webhooksRe-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.
Last updated on