Files

8.0 KiB

Parallel.ai Auto-Registration (2026-07-20)

Platform Overview

Auth Flow (Fully Reverse-Engineered)

Step 1: Get CSRF Token

GET https://platform.parallel.ai/api/auth/csrf
→ {"csrfToken":"..."}

Saves cookie: next-auth.csrf-token

POST https://platform.parallel.ai/api/auth/signin/email
Content-Type: application/x-www-form-urlencoded
Body: email=<email>&csrfToken=<token>&callbackUrl=https://platform.parallel.ai/post-auth-sign-up
→ 302 → /api/auth/verify-request?provider=email&type=email

The callbackUrl determines post-auth redirect:

  • /post-auth-sign-up → new user onboarding
  • / → existing user dashboard

Email arrives from noreply@parallel.ai with subject "Log in to Parallel Web Systems". The body contains: https://platform.parallel.ai/redirect?to=<encoded_callback_url>

Decode the callback URL:

from urllib.parse import unquote
to_match = re.search(r'to=([^&]+)', redirect_url)
callback_url = unquote(to_match.group(1))
# → https://platform.parallel.ai/api/auth/callback/email?callbackUrl=...&token=[REDACTED]

Pitfall: The token in the callback URL is SINGLE-USE and expires in ~2 minutes. Do NOT send the magic link and then poll IMAP for 30 seconds — the token will expire. The IMAP polling must be fast (<15 seconds).

Pitfall: If the same email is used for multiple signup attempts, the inbox will have multiple Parallel emails. ALWAYS filter by email date — only use emails received AFTER the magic link was sent:

sent_time = datetime.now(timezone.utc)
# ... in IMAP polling loop:
msg_date = parsedate_to_datetime(msg.get('Date'))
if msg_date and msg_date > sent_time:
    # This is our email

Step 4: Follow Callback

GET https://platform.parallel.ai/api/auth/callback/email?callbackUrl=...&token=[REDACTED]
→ 302 → /post-auth-sign-up → 307 → /onboard

Sets session cookie: st=<JWT>

Step 5: Verify Session

GET https://platform.parallel.ai/api/auth/session
→ {"user":{"email":"..."},"expires":"...","accountServiceUserId":"..."}

Step 6: Create Organization

POST https://platform.parallel.ai/api/rpc/onboard/create-org-with-visibility
Content-Type: application/json
Body: {"orgName":"Research","visibility":"private"}
→ {"kind":"ok","data":{"success":true,"data":{"org_id":"...","org_name":"..."}}}

Pitfall: visibility field name is correct. name or org_name will fail with "Invalid input: expected string, received undefined". The field is orgName.

Step 7: Get API Key

POST https://platform.parallel.ai/api/rpc/auth/get-my-api-key-and-session-details
Content-Type: application/json
Body: {}
→ {"kind":"ok","data":{"apiKey":"...","orgDomain":"...","baseUrl":"https://api.parallel.ai/"}}

Pitfall: This endpoint requires POST with JSON body (even empty {}). GET returns 405. POST without Content-Type returns 415.

Step 8: Verify API Key

POST https://api.parallel.ai/v1/search
Authorization: Bearer <api_key>
Content-Type: application/json
Body: {"search_queries":["test"]}
→ 200 = valid key (may return "Insufficient credit" if $0 balance, but key IS valid)
→ 401 = invalid/expired key

Credit System

Domain Classification

POST https://platform.parallel.ai/api/rpc/onboard/check-email-domain
Body: {"email":"user@domain.com"}
→ {"domainType":"public"|"business"|"private","isPublicOrTemp":true|false}
  • Business/private domains (e.g., @company.com, @reforrm.me): $80 credits, no card required (user confirmed)
  • Public domains (e.g., @gmail.com, @t-online.de, @yahoo.com): $0 credits, may require card for $5/mo free tier
  • BUT: t-online.de classified as domainType: "public" yet ~89% of registered accounts (17/19 tested) still get working credits despite $0 balance shown. The credit check is inconsistent — some accounts work, some don't.

Balance Check

POST https://platform.parallel.ai/api/rpc/billing/payments/get-balance-due
Body: {}
→ {"kind":"ok","data":{"balance":0,"pendingDebitBalanceCents":0}}

All Discovered RPC Endpoints

Onboard Page (chunk: page-2a73a49c346d7f15.js)

  • POST /api/rpc/onboard/check-email-domain — {"email":"..."} → domain classification
  • POST /api/rpc/onboard/create-org-with-visibility — {"orgName":"...","visibility":"private"} → org_id
  • POST /api/rpc/onboard/get-user-orgs — {} → list of orgs
  • POST /api/rpc/onboard/set-selected-org — {"orgId":"..."} → select org
  • POST /api/rpc/onboard/create-join-request — join existing org
  • POST /api/rpc/onboard/discover-orgs — find orgs by domain
  • POST /api/rpc/onboard/invite-users — invite to org
  • POST /api/rpc/onboard/set-org-domain-status — domain verification
  • POST /api/rpc/onboard/preload-org-data-for-home — dashboard data
  • POST /api/rpc/onboard/get-user-join-requests — pending requests

Settings/Nav (chunk: 34932-6935ecb29a09831c.js)

  • POST /api/rpc/auth/get-my-api-key-and-session-details — get API key (POST + JSON required)
  • POST /api/rpc/auth/refresh-my-api-key-and-session-details — refresh key (POST + JSON)
  • POST /api/rpc/auth/ensure-selected-org — {"selectedOrgId":"..."} — set active org
  • POST /api/rpc/billing/payments/get-balance-due — balance check
  • POST /api/rpc/billing/payments/has-payment-method — {"hasPaymentMethod":false}
  • POST /api/rpc/billing/payments/refresh-balance — needs selected org
  • POST /api/rpc/onboard/clear-bootstrap-cookies — reset onboarding
  • POST /api/rpc/staging/enable-staging-auto-route — staging feature
  • POST /api/rpc/staging/disable-staging-auto-route — staging feature

Reverse-Engineering Methodology

How to Find API Endpoints in Next.js Apps

  1. Get the page's JS chunks from the HTML source: re.findall(r'/_next/static/chunks/[^"]+\.js[^"]*', html)
  2. Identify the page-specific chunk: usually named page-*.js or has the page name
  3. Download and grep for RPC endpoints: re.findall(r'"/api/rpc/[^"]*"', chunk_text)
  4. Test endpoints with the session cookie from a valid auth flow

How to Find Field Names (When API Returns "expected string, received undefined")

The error messages tell you the exact field name:

"fieldErrors":{"orgName":["Invalid input: expected string, received undefined"]}

→ The field is orgName, NOT name, org_name, or organization.

import imaplib, ssl

ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE

mail = imaplib.IMAP4_SSL('imap.t-online.de', 993, ssl_context=ctx, timeout=10)
mail.login('[REDACTED-EMAIL]', 'password')
mail.select('INBOX')

# CRITICAL: call noop() before each search to refresh mailbox
mail.noop()
status, messages = mail.search(None, 'ALL')

Pitfall: t-online.de SSL certificate fails standard verification. MUST use ssl_context with check_hostname=False and verify_mode=CERT_NONE. Without this, ssl.SSLCertVerificationError.

Script Template

See templates/parallel_autoreg.py for the full working registration script. See templates/parallel_proxy.py for the API key rotator proxy (retries on 402/429).

Production instances (2026-07-20):

  • Registration: C:\\Users\\User\\Documents\\parallel_autoreg.py
  • Proxy: C:\\Users\\User\\Documents\\parallel_proxy.py (port 8765, 70 keys)
  • Parser: C:\\Users\\User\\Documents\\parallel_parse.py
  • Deduped email file: C:\\Users\\User\\Documents\\t-online_deduped.txt (9764 unique emails)
  • Source: C:\\Users\\User\\Desktop\\Downloads_backup_01.07.2026\\working_mails_RECOVERED.txt (9800 lines)
  • Output keys: C:\\Users\\User\\Documents\\parallel_keys.json (70 keys)
  • API keys only: C:\\Users\\User\\Documents\\parallel_api_keys.txt