Source: https://developers.tokportal.com/comments-review-flow/
Markdown: https://developers.tokportal.com/comments-review-flow.md

# Approve & Dispute

When the auto-verifier can't find a comment but the manager swears they posted it, the task moves to **`manually_confirmed`** and you get a 72h window to:

- **Approve** — pay the manager immediately, skip the wait.
- **Dispute** — send a reason back to the manager so they fix it. The task flips to `pending_corrections`; once they re-verify, it moves on.

If you do nothing for 72 hours, the task auto-finalizes (the manager is paid).

## Approve

```
POST /api/ext/comments/{id}/approve
```

```bash
curl -X POST https://app.tokportal.com/api/ext/comments/0d8b5a3e-92c4-4111-9a7d-3e2f1a2b3c4d/approve \
  -H "X-API-Key: sk_xxx"
```

**Response (200):**

```json
{
  "data": {
    "id": "0d8b5a3e-92c4-4111-9a7d-3e2f1a2b3c4d",
    "status": "finalized",
    "platform": "tiktok",
    "cm_payout_amount": 0.24,
    "submitted_at": "2026-04-27T17:21:34Z",
    "manually_confirmed_at": "2026-04-28T13:56:11Z",
    "finalized_at": "2026-04-28T13:56:17Z",
    "...": "..."
  }
}
```

The payout is computed from `submitted_at → manually_confirmed_at` (the same speed-tier table as auto-verified tasks).

### Errors

- `404 COMMENT_TASK_NOT_FOUND` — bad ID.
- `403 COMMENT_TASK_NOT_OWNED` — belongs to another user.
- `409 COMMENT_INVALID_STATUS` — task isn't in `manually_confirmed`. `details.current_status` tells you where it actually is.

## Dispute

```
POST /api/ext/comments/{id}/dispute
```

Body:

| Field    | Type   | Required | Description                                                                         |
| -------- | ------ | -------- | ----------------------------------------------------------------------------------- |
| `reason` | string | yes      | 3–500 characters. The manager will see this verbatim and use it to fix the comment. |

```bash
curl -X POST https://app.tokportal.com/api/ext/comments/0d8b5a3e-92c4-4111-9a7d-3e2f1a2b3c4d/dispute \
  -H "X-API-Key: sk_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "reason": "Cannot find the comment under the video, please re-post."
  }'
```

**Response (200):**

```json
{
  "data": {
    "id": "0d8b5a3e-92c4-4111-9a7d-3e2f1a2b3c4d",
    "status": "pending_corrections",
    "correction_required": "The client reviewed your comment and reported a problem:\n\n\"Cannot find the comment under the video, please re-post.\"\n\nPlease fix the issue and click \"I fixed it — re-check\" once done.",
    "manually_confirmed_at": null,
    "client_dispute_deadline_at": null,
    "...": "..."
  }
}
```

The task is now in `pending_corrections`. The manager fixes it and re-runs the auto-verifier — on success the task lands in `verified_auto` (or `manually_confirmed` again, if the verifier still can't see it). You can keep iterating: dispute again, approve, or just wait.

### Errors

- `400 COMMENT_DISPUTE_REASON_REQUIRED` — `reason` missing or too short.
- `409 COMMENT_INVALID_STATUS` — task isn't in `manually_confirmed`.
- All the standard 401/403/404s.

## A complete review loop in pseudo-code

```python


KEY = "sk_xxx"
BASE = "https://app.tokportal.com/api/ext"

def review_pending():
    r = requests.get(f"{BASE}/comments?status=manually_confirmed",
                     headers={"X-API-Key": KEY}).json()
    for task in r["data"]:
        timeline = requests.get(
            f"{BASE}/comments/{task['id']}/verifications",
            headers={"X-API-Key": KEY}).json()["data"]

        # Custom logic — e.g. auto-approve if last attempt's
        # comments_scraped is 0 (likely an Apify miss, not a real no-show).
        last = timeline["attempts"][-1] if timeline["attempts"] else None
        if last and last["comments_scraped"] == 0:
            requests.post(f"{BASE}/comments/{task['id']}/approve",
                          headers={"X-API-Key": KEY})
        else:
            requests.post(f"{BASE}/comments/{task['id']}/dispute",
                          headers={"X-API-Key": KEY,
                                   "Content-Type": "application/json"},
                          json={"reason": "Cannot locate the comment under the video."})

review_pending()
```
