Tutorials

Notion API + CaptchaAI: entrada automatizada de dados com tratamento CAPTCHA

Um board do Notion vira fila de tarefas de CAPTCHA com poucas linhas de código: leia os registros pendentes pela API do Notion, envie a sitekey e a URL para a CaptchaAI, grave o token de volta na mesma linha. É esse fluxo — sem trocar de ferramenta, sem planilha paralela — que este tutorial monta em Python e em JavaScript.

Times de operações já usam o Notion para controlar backlog; reaproveitar o mesmo board para orquestrar CAPTCHA evita subir mais uma ferramenta. A API do Notion lê e grava os registros; a CaptchaAI resolve o desafio reCAPTCHA v2 que trava o formulário.

Antes de começar

Requisito Detalhe
Integração do Notion Integração interna, criada em developers.notion.com
Banco de dados no Notion Compartilhado com essa integração
Chave de API Da CaptchaAI
Ambiente Python 3.8+ ou Node.js 18+

Por que usar o Notion como fila de tarefas para CAPTCHA

Imagine uma equipe de precificação em São Paulo que mantém, no Notion, uma lista de páginas de fornecedores para monitorar preços todos os dias. Parte dessas páginas pede reCAPTCHA v2 antes de liberar o conteúdo. Um script batendo direto nelas trava assim que encontra o desafio; a mesma automação, apontando para o Notion como fila, continua rodando sozinha:

  1. Lê as tarefas com status pendente no Notion
  2. Resolve o CAPTCHA de cada uma via CaptchaAI
  3. Grava o token e o novo status de volta no registro

Como o worker roda como um job — não em navegador —, os servidores costumam ficar perto da região sa-east-1 da AWS para reduzir a latência até a API do Notion. Se as URLs vierem de bases internas com dados reais, considere a LGPD antes de gravar algo sensível no board.

Monte a base de dados no Notion

Crie um banco de dados no Notion com estas propriedades, usando exatamente estes nomes — o Notion diferencia maiúsculas de minúsculas:

Propriedade Tipo Finalidade
Name Título Identificador da tarefa
URL URL Página de destino com o CAPTCHA
Sitekey Texto rico sitekey do reCAPTCHA v2
Status Seleção Pending, Solving, Solved, Failed
Token Texto rico Token do CAPTCHA resolvido
Solved At Data Carimbo de data/hora da resolução
Error Texto rico Mensagem de erro em caso de falha

Compartilhe o banco com a integração antes de rodar o worker — sem isso, toda chamada retorna 401.

Erros comuns e como corrigir

Antes de escrever o worker, vale conhecer os erros mais frequentes desse fluxo — a maioria vem de um detalhe de configuração no Notion, não do código:

Problema Causa Correção
401 Unauthorized do Notion Integração não conectada ao banco de dados Compartilhe o banco de dados com a integração no Notion
Nome de propriedade não bate O Notion diferencia maiúsculas de minúsculas Use exatamente os nomes da tabela acima — Sitekey, não sitekey ou Site Key
Token cortado no meio Limite do Notion de 2.000 caracteres em campos rich_text Tokens de CAPTCHA normalmente têm menos de 1.000 caracteres — na prática isso não deve dar problema
Limite de requisições do Notion (429) Muitas chamadas seguidas à API Adicione um atraso de 1 segundo entre as atualizações no Notion

Três hábitos evitam a maior parte desses problemas em produção:

  • Rode um teste com uma única tarefa antes de processar o lote inteiro
  • Monitore o campo Error para identificar padrões de falha por fornecedor
  • Aumente o intervalo entre chamadas se o Notion começar a retornar 429 com frequência

Implementação em Python

O worker consulta o Notion por tarefas com Status = Pending, resolve cada CAPTCHA na CaptchaAI e grava o resultado de volta no mesmo registro.

# notion_captcha_worker.py
import os
import time
import requests

NOTION_TOKEN = os.environ.get("NOTION_TOKEN")
NOTION_DB_ID = os.environ.get("NOTION_DB_ID")
CAPTCHAAI_KEY = os.environ.get("CAPTCHAAI_KEY", "YOUR_API_KEY")

NOTION_HEADERS = {
    "Authorization": f"Bearer {NOTION_TOKEN}",
    "Content-Type": "application/json",
    "Notion-Version": "2022-06-28",
}

def get_pending_tasks():
    """Fetch tasks with Status = Pending from Notion."""
    url = f"https://api.notion.com/v1/databases/{NOTION_DB_ID}/query"
    payload = {
        "filter": {
            "property": "Status",
            "select": {"equals": "Pending"},
        }
    }
    resp = requests.post(url, headers=NOTION_HEADERS, json=payload)
    resp.raise_for_status()
    return resp.json()["results"]

def update_task(page_id, properties):
    """Update a Notion page with new property values."""
    url = f"https://api.notion.com/v1/pages/{page_id}"
    payload = {"properties": properties}
    resp = requests.patch(url, headers=NOTION_HEADERS, json=payload)
    resp.raise_for_status()

def set_status(page_id, status, token=None, error=None):
    """Update task status in Notion."""
    props = {"Status": {"select": {"name": status}}}

    if token:
        props["Token"] = {"rich_text": [{"text": {"content": token[:2000]}}]}
        props["Solved At"] = {"date": {"start": time.strftime("%Y-%m-%dT%H:%M:%S")}}

    if error:
        props["Error"] = {"rich_text": [{"text": {"content": error[:200]}}]}

    update_task(page_id, props)

def solve_captcha(sitekey, pageurl):
    """Submit to CaptchaAI and poll for result."""
    # Submit
    resp = requests.get("https://ocr.captchaai.com/in.php", params={
        "key": CAPTCHAAI_KEY,
        "method": "userrecaptcha",
        "googlekey": sitekey,
        "pageurl": pageurl,
        "json": "1",
    })
    result = resp.json()

    if result.get("status") != 1:
        raise Exception(f"Submit failed: {result.get('request')}")

    task_id = result["request"]

    # Poll
    time.sleep(15)
    for _ in range(25):
        poll = requests.get("https://ocr.captchaai.com/res.php", params={
            "key": CAPTCHAAI_KEY,
            "action": "get",
            "id": task_id,
            "json": "1",
        })
        poll_result = poll.json()

        if poll_result.get("status") == 1:
            return poll_result["request"]
        if poll_result.get("request") != "CAPCHA_NOT_READY":
            raise Exception(f"Solve failed: {poll_result.get('request')}")

        time.sleep(5)

    raise Exception("Polling timeout")

def extract_property(page, prop_name, prop_type="rich_text"):
    """Extract a property value from a Notion page."""
    prop = page["properties"].get(prop_name, {})
    if prop_type == "rich_text":
        texts = prop.get("rich_text", [])
        return texts[0]["plain_text"] if texts else ""
    elif prop_type == "url":
        return prop.get("url", "")
    return ""

def main():
    tasks = get_pending_tasks()
    print(f"Found {len(tasks)} pending tasks")

    for task in tasks:
        page_id = task["id"]
        sitekey = extract_property(task, "Sitekey")
        pageurl = extract_property(task, "URL", "url")

        if not sitekey or not pageurl:
            set_status(page_id, "Failed", error="Missing sitekey or URL")
            continue

        print(f"Solving: {pageurl}")
        set_status(page_id, "Solving")

        try:
            token = solve_captcha(sitekey, pageurl)
            set_status(page_id, "Solved", token=token)
            print(f"  Solved successfully")
        except Exception as e:
            set_status(page_id, "Failed", error=str(e))
            print(f"  Failed: {e}")

        time.sleep(1)  # Rate limit for Notion API

    print("All tasks processed")

if __name__ == "__main__":
    main()

Implementação em JavaScript

A mesma lógica em Node.js, usando o SDK oficial @notionhq/client junto com axios para a chamada à CaptchaAI.

// notion_captcha_worker.js
const { Client } = require('@notionhq/client');
const axios = require('axios');

const notion = new Client({ auth: process.env.NOTION_TOKEN });
const DB_ID = process.env.NOTION_DB_ID;
const API_KEY = process.env.CAPTCHAAI_KEY || 'YOUR_API_KEY';

async function getPendingTasks() {
  const response = await notion.databases.query({
    database_id: DB_ID,
    filter: { property: 'Status', select: { equals: 'Pending' } },
  });
  return response.results;
}

async function updateTask(pageId, status, token, error) {
  const properties = {
    Status: { select: { name: status } },
  };
  if (token) {
    properties.Token = { rich_text: [{ text: { content: token.slice(0, 2000) } }] };
    properties['Solved At'] = { date: { start: new Date().toISOString() } };
  }
  if (error) {
    properties.Error = { rich_text: [{ text: { content: error.slice(0, 200) } }] };
  }
  await notion.pages.update({ page_id: pageId, properties });
}

async function solveCaptcha(sitekey, pageurl) {
  const submit = await axios.get('https://ocr.captchaai.com/in.php', {
    params: {
      key: API_KEY, method: 'userrecaptcha',
      googlekey: sitekey, pageurl, json: '1',
    },
  });
  if (submit.data.status !== 1) throw new Error(submit.data.request);

  await new Promise(r => setTimeout(r, 15000));

  for (let i = 0; i < 25; i++) {
    const poll = await axios.get('https://ocr.captchaai.com/res.php', {
      params: { key: API_KEY, action: 'get', id: submit.data.request, json: '1' },
    });
    if (poll.data.status === 1) return poll.data.request;
    if (poll.data.request !== 'CAPCHA_NOT_READY') throw new Error(poll.data.request);
    await new Promise(r => setTimeout(r, 5000));
  }
  throw new Error('Timeout');
}

async function main() {
  const tasks = await getPendingTasks();
  console.log(`Found ${tasks.length} pending tasks`);

  for (const task of tasks) {
    const sitekey = task.properties.Sitekey?.rich_text?.[0]?.plain_text;
    const pageurl = task.properties.URL?.url;

    if (!sitekey || !pageurl) {
      await updateTask(task.id, 'Failed', null, 'Missing sitekey or URL');
      continue;
    }

    console.log(`Solving: ${pageurl}`);
    await updateTask(task.id, 'Solving');

    try {
      const token = await solveCaptcha(sitekey, pageurl);
      await updateTask(task.id, 'Solved', token);
      console.log('  Solved');
    } catch (e) {
      await updateTask(task.id, 'Failed', null, e.message);
      console.log(`  Failed: ${e.message}`);
    }

    await new Promise(r => setTimeout(r, 1000));
  }
}

main().catch(console.error);

Perguntas frequentes

Por que usar o Notion em vez de um banco de dados tradicional?

A equipe já mexe nele. Pausar ou adicionar uma URL vira editar uma linha, sem tela de admin separada — suficiente para filas de dezenas ou centenas de tarefas por rodada.

Dá para deixar isso rodando sozinho, num agendamento?

Sim. Cron, Agendador de Tarefas do Windows ou um agendador de nuvem (AWS EventBridge, Google Cloud Scheduler) chamam o script no intervalo definido — o worker processa tudo que estiver Pending.

O token do reCAPTCHA expira se eu demorar para usar?

Sim, tokens do reCAPTCHA v2 duram poucos minutos. Grave o token e envie ao formulário logo em seguida — não deixe uma tarefa "Solved" parada na fila.

O fluxo funciona com outros tipos de CAPTCHA além do reCAPTCHA v2?

Sim. Adicione uma propriedade "Tipo de CAPTCHA" ao banco e ajuste a função de resolução para o método correspondente da CaptchaAI:

  • turnstile — Cloudflare Turnstile
  • geetest — GeeTest v3
  • base64 — CAPTCHAs de imagem/OCR

Preciso de um plano específico da CaptchaAI para esse volume?

Não necessariamente. A CaptchaAI cobra por thread simultânea, não por solução: o BASIC (US$ 15/mês, 5 threads) já cobre filas pequenas no Notion. Se o volume crescer, suba para um plano com mais threads.

Artigos relacionados

Coloque em produção

Transforme o board do Notion em fila automática de CAPTCHA — crie sua chave de API da CaptchaAI.

Guias relacionados:

Os comentários estão desativados para este artigo.