Skip to the content.

Fix members:export — “401 email verification required”

Your cookies are fine. auth:login works, posts:list works, system:debug works — and members:export alone comes back with 400 invalid client ID or 401 email verification required.

That is not a broken export. On 30 June 2026 Skool reclassified the member CSV bulk export as a sensitive action. Two things changed at once:

  1. The export request must now carry a client_id in the query string.
  2. That client_id must be email-verified — Skool emails a numeric code, and the id only counts as trusted once you submit it.

Nothing else about authentication changed, which is exactly why the failure looks so isolated: every other endpoint keeps working on the same cookies.

Quick reference (TL;DR for agents)

   
Symptom members:export returns 400 invalid client ID or 401 email verification required while every other action works
Cause Skool’s sensitive-action gate: the export needs an email-verified client_id
Fix Pin one client_id, verify it once with a code Skool emails you, reuse it
Recurring? Yes — verification lapses after ~2 days
Automatable? Fully. The code arrives by email, so a programmatic inbox closes the loop
Actor version 0.3.34+ sends the client_id. Verifying it is on your side (for now)

Why the error changes shape

Depending on which actor build you are on, the same root cause shows up differently. Both mean “this client_id is not trusted”:

Error What it means
400 — invalid client ID No client_id reached the endpoint at all. Actor builds before 0.3.34
401 — email verification required A client_id arrived, but it has never been verified — or its verification lapsed

If you are seeing the 400, upgrade first: builds from 0.3.34 onward forward the client_id from your cookies. Then work through the 401 below.

The trap: auth:login gives you a new client_id every time

This is the part that makes the problem feel unfixable.

Each auth:login call mints a fresh, random client_id, which Skool treats as a brand-new, untrusted device. So the intuitive fix — “log in again to get clean cookies” — guarantees failure: you verify one id, then hand the export a different, unverified one on the next run.

Pin one client_id and keep reusing it. Verification binds to the account, not to the session or the auth token, so a verified id keeps working across logins until it lapses.

Prerequisites

Step 1 — Get a client_id and hold on to it

Call auth:login once, take the client_id out of the returned cookie string, and store it wherever you keep config:

curl -s -X POST "https://api.apify.com/v2/acts/cristiantala~skool-all-in-one-api/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "action": "auth:login",
    "groupSlug": "your-community",
    "email": "you@example.com",
    "password": "your-password"
  }'

The response contains a cookies string of the form auth_token=…; client_id=…; aws-waf-token=…. The client_id in it is the one you are about to verify.

Step 2 — Trigger the verification code

Two plain HTTP calls against api2.skool.com, using those same cookies. Both require the full cookie triple, the x-aws-waf-token header, and a browser User-Agent — Skool rejects them otherwise.

curl -s -X POST "https://api2.skool.com/auth/email-verify-init" \
  -H "Content-Type: application/json" \
  -H "Cookie: auth_token=$AUTH_TOKEN; client_id=$CLIENT_ID; aws-waf-token=$WAF_TOKEN" \
  -H "x-aws-waf-token: $WAF_TOKEN" \
  -H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" \
  -d "{\"client_id\": \"$CLIENT_ID\"}"

Expect 200 {"verified": false}. That response means the code was sent, not that anything failed.

Skool emails a numeric code with a subject like 1234 is your Skool email verification code. It expires in about 30 minutes.

Step 3 — Submit the code

curl -s -X POST "https://api2.skool.com/auth/email-verify" \
  -H "Content-Type: application/json" \
  -H "Cookie: auth_token=$AUTH_TOKEN; client_id=$CLIENT_ID; aws-waf-token=$WAF_TOKEN" \
  -H "x-aws-waf-token: $WAF_TOKEN" \
  -H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" \
  -d "{\"client_id\": \"$CLIENT_ID\", \"code\": \"1234\"}"

200 means that client_id is now trusted for your account.

Step 4 — Run the export with those cookies

Pass the cookie string containing the verified client_id:

curl -s -X POST "https://api.apify.com/v2/acts/cristiantala~skool-all-in-one-api/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "action": "members:export",
    "groupSlug": "your-community",
    "cookies": "auth_token=…; client_id=…; aws-waf-token=…",
    "params": { "status": "active" }
  }'

From here the normal export recipe applies.

Step 5 — Automate it, because it lapses every ~2 days

This is not one-time setup. Verification expires after roughly two days, and when it does the export starts failing again — silently, from the perspective of a nightly job.

The good news: the code arrives by email, so nothing here needs a human. The loop is:

  1. Run the export.
  2. If it fails with email verification required (or invalid client ID), fire email-verify-init.
  3. Read the code from the inbox.
  4. Post email-verify.
  5. Retry the export once. If it still fails, stop and alert — do not degrade silently.

Any programmatic inbox works: Gmail API, plain IMAP, or a dedicated domain on Cloudflare Email Routing pointed at a worker. What matters is not the provider but the filtering:

Reference implementation shape (Node):

// Read the body ONCE. Calling r.json() and then r.text() throws
// "body stream already read" — a real gotcha when you add logging here.
const res = await fetch(url, { method: 'POST', headers, body });
const raw = await res.text();
const data = JSON.parse(raw);

Fail closed, not open

If verification cannot be completed, abort the job rather than continuing with partial data. An export that silently returns nothing looks identical to a community that lost all its members — and if you are reconciling paid status from it, that difference matters a lot.

Gotchas

→ Open the actor on Apify

New to Skool? Launch your community here — 14-day free trial.