---
title: "POST com JSON no Python Requests: equivalentes do cURL"
description: "Para enviar JSON via POST no Python Requests, chame requests.post(url, json=data) e o Content-Type é definido para você. Veja data=, params=, headers e o cURL."
url: https://proxynet.io/pt-br/blog/python-requests-post-json
date: 2026-09-25
author: "Acar Diveroli"
category: "Tutoriais, Web scraping"
lang: pt-BR
---

# POST com JSON no Python Requests: equivalentes do cURL

A documentação da API de uma transportadora mostra como criar um envio com um comando cURL: `curl -X POST https://api.example.com/v1/shipments -H "Authorization: Bearer TOKEN" -H "Content-Type: application/json" -d '{"recipient": "Ayse Demir", "weight_kg": 3}'`. No terminal, a resposta é `201 Created`. Você passa o comando para o Python como `requests.post(url, data=json.dumps(body), headers={"Authorization": ...})`, e o servidor responde `415 Unsupported Media Type`: com uma string em `data=`, o Requests não envia `Content-Type`, e a linha que definia esse cabeçalho ficou no comando cURL.

Este guia trata de `json=`, `data=` e `files=`, dos parâmetros de consulta, dos cabeçalhos e tokens Bearer, da leitura da resposta e do argumento do Requests para cada opção comum do cURL. No final há uma lista de verificação para requisições que funcionam no cURL mas não no Python, e um pequeno cliente de API que tenta de novo com segurança. Todos os exemplos rodaram no Python 3.13.9, Requests 2.34.2 e curl 8.21.0 (Windows 11), contra o httpbin.org e um servidor local que devolve os bytes que recebe.

> **Nota: Resposta rápida**
>
> Para enviar JSON via POST com o Python Requests, chame `requests.post(url, json=data, timeout=20)`. O Requests converte o dict em JSON e define `Content-Type: application/json` sozinho. `data=` com um dict envia um formulário, `data=` com uma string envia texto sem `Content-Type`, e `files=` envia `multipart/form-data`. A partir de um comando cURL, as linhas `-H` vão para `headers=`, os valores de consulta de `-G` para `params=`, `-u` para `auth=`, `-F` para `files=` e `-x` para `proxies=`. Se a requisição convertida receber uma resposta diferente, confira primeiro os redirecionamentos: o cURL só os segue com `-L`, o Requests os segue por padrão.

## Como enviar uma requisição POST com o Python Requests?

Instale o pacote com `pip install requests`. O [PyPI](https://pypi.org/project/requests/) lista a 2.34.2, lançada em 14 de maio de 2026, como versão atual; ela exige Python 3.10 ou mais recente. A escolha entre bibliotecas está em [Comparativo entre HTTPX, Requests e AIOHTTP](/pt-br/blog/httpx-vs-requests-vs-aiohttp).

Um POST com corpo JSON é uma única chamada. O httpbin.org/post devolve o que recebeu:

```python
import requests

payload = {"recipient": "Ayse Demir", "weight_kg": 3}
r = requests.post("https://httpbin.org/post", json=payload, timeout=20)

print(r.status_code)                         # 200
print(r.json()["headers"]["Content-Type"])   # application/json
print(r.json()["data"])                      # {"recipient": "Ayse Demir", "weight_kg": 3}
print(r.json()["json"])                      # o mesmo corpo, convertido de volta em dict
```

O campo `data` é o corpo como foi enviado, com um espaço depois de cada dois-pontos e de cada vírgula. `requests.put()`, `requests.patch()` e `requests.delete()` aceitam os mesmos argumentos. Passe sempre `timeout`: o Requests não tem valor padrão, então um servidor que não responde deixa o script esperando ([Max Retries Exceeded With URL](/pt-br/blog/max-retries-exceeded-with-url) trata dos erros de tempo limite).

## Qual é a diferença entre json=, data= e files=?

Cada argumento monta o corpo de um jeito e define um `Content-Type` diferente. Enviamos o mesmo dict de quatro formas:

```python
import json
import requests

url = "https://httpbin.org/post"
body = {"recipient": "Ayse Demir", "weight_kg": 3}

for label, kwargs in [
    ("json=body", {"json": body}),
    ("data=body", {"data": body}),
    ("data=json.dumps(body)", {"data": json.dumps(body)}),
    ("json= and data=", {"json": body, "data": {"note": "x"}}),
]:
    echo = requests.post(url, timeout=20, **kwargs).json()
    print(f"{label:22} {echo['headers'].get('Content-Type')!s:34} form={echo['form']} json={echo['json']}")
```

```text
json=body              application/json                   form={} json={'recipient': 'Ayse Demir', 'weight_kg': 3}
data=body              application/x-www-form-urlencoded  form={'recipient': 'Ayse Demir', 'weight_kg': '3'} json=None
data=json.dumps(body)  None                               form={} json={'recipient': 'Ayse Demir', 'weight_kg': 3}
json= and data=        application/x-www-form-urlencoded  form={'note': 'x'} json=None
```

O que as quatro linhas mostram:

- **`json=`** serializa o dict e define `Content-Type: application/json`. Use para APIs JSON.
- **`data=` com um dict** envia um formulário, e todo valor vira texto: `weight_kg` chegou como `'3'`.
- **`data=` com uma string** não envia `Content-Type`. O httpbin interpretou o corpo mesmo assim; uma API rigorosa responde `415` ou `400`. Defina o cabeçalho você mesmo.
- **`json=` junto com `data=` ou `files=`** perde o JSON sem nenhum erro. O [guia rápido do Requests](https://requests.readthedocs.io/en/latest/user/quickstart/) diz que o parâmetro `json` é ignorado quando `data` ou `files` é passado.

Envie texto JSON por `data=` só quando os bytes exatos importam. Uma API que assina o corpo com HMAC confere os bytes que recebe, e `json=` acrescenta espaços e escapa caracteres não ASCII (`"İzmir"` sai como `"\u0130zmir"`). Monte os bytes você mesmo:

```python
import json
import requests

body = {"recipient": "Ayse Demir", "city": "İzmir", "weight_kg": 3}
raw = json.dumps(body, separators=(",", ":"), ensure_ascii=False).encode("utf-8")

r = requests.post("https://httpbin.org/post", data=raw,
                  headers={"Content-Type": "application/json"}, timeout=20)
print(r.json()["data"])  # {"recipient":"Ayse Demir","city":"İzmir","weight_kg":3}
```

Esses bytes tiveram o mesmo hash SHA-256 do corpo que `curl --data-binary @body.json` enviou a partir do mesmo arquivo UTF-8. Logins por formulário com tokens CSRF estão em [Sessões e cookies em Python](/pt-br/blog/python-login-session-cookies), e `files=` mais abaixo.

## Como converter um comando cURL para o Requests, passo a passo?

A mesma requisição, copiada na aba Rede (Network) do navegador enquanto você usa o painel web da transportadora, tem algumas linhas a mais. Como encontrar uma requisição assim está em [Páginas estáticas e dinâmicas no web scraping](/pt-br/blog/static-vs-dynamic-pages).

```bash
curl -X POST "https://api.example.com/v1/shipments?notify=false" \
  -H "Authorization: Bearer TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept-Encoding: gzip, deflate, br" \
  -H "Cookie: session=abc123" \
  --data-raw '{"recipient": "Ayse Demir", "weight_kg": 3}'
```

1. **Desfaça a sintaxe do shell.** Remova as quebras de linha com `\` (`^` numa cópia feita para o `cmd` do Windows) e as aspas.
2. **Leve a query string para `params=`.** `?notify=false` vira `params={"notify": "false"}`.
3. **Filtre os cabeçalhos.** Mantenha o que a API precisa: `Authorization`, `Accept`, chaves de API. Tire `Accept-Encoding`, que o Requests trata sozinho, e `Content-Type` quando usar `json=`. Nunca copie `Host` nem `Content-Length`; o Requests calcula os dois. Linhas do navegador como `sec-fetch-*` raramente são necessárias, e uma linha `Cookie` vai para `cookies=` ou para uma `Session`.
4. **Escolha o argumento do corpo.** JSON vira `json=` (ou `data=` com bytes exatos, se o corpo for assinado), `-d "a=1&b=2"` vira `data={"a": "1", "b": "2"}`, e `-F` vira `files=`.
5. **Escolha o método.** `-d`, `--data-raw`, `--json` e `-F` significam POST, a menos que `-X` diga outra coisa; `-G` transforma a requisição em uma consulta GET.
6. **Adicione um `timeout=`** e chame `r.raise_for_status()`.
7. **Compare.** Envie o comando cURL e a sua chamada Python para `https://httpbin.org/anything` e compare o método, a URL, os cabeçalhos e o corpo devolvidos.

O resultado, com o token lido de uma variável de ambiente:

```python
import os
import requests

r = requests.post(
    "https://api.example.com/v1/shipments",
    params={"notify": "false"},
    headers={"Authorization": f"Bearer {os.environ['API_TOKEN']}"},
    json={"recipient": "Ayse Demir", "weight_kg": 3},
    timeout=20,
)
r.raise_for_status()
print(r.status_code, r.headers.get("Location"))
```

O [curlconverter](https://github.com/curlconverter/curlconverter) faz os passos 1 a 5 e tem o Python Requests como saída padrão. Instale com npm e troque `curl` por `curlconverter` no comando. Para o comando acima, a versão 4.12.0 levou `notify` para `params`, o cookie para `cookies=` e o corpo para `json=`, e deixou `Content-Type` e `Accept-Encoding` comentados. Não acrescentou tempo limite, e o README avisa que o código gerado segue redirecionamentos, a menos que o comando defina uma política de redirecionamento. Um comando copiado do navegador contém o seu cookie de sessão ativo e o seu token, então converta na sua máquina, não em um site.

## Qual argumento do Requests corresponde a cada opção do cURL?

| Opção do cURL | Requests | O que muda |
|---|---|---|
| `-d '{"a":1}'` com `Content-Type: application/json` | `json={"a": 1}` | O Requests acrescenta espaços; use `data=` para bytes exatos |
| `--json '{"a":1}'` | `json={"a": 1}`, `headers={"Accept": "application/json"}` | `--json` (curl 7.82.0+) também define `Accept` |
| `-d "a=1&b=2"` | `data={"a": "1", "b": "2"}` | Mesmos bytes |
| `--data-binary @body.json` | `data=open("body.json", "rb")` | `-d @file` removeria as quebras de linha; estes dois as mantêm |
| `-F "file=@report.csv"` | `files={"file": open("report.csv", "rb")}` | O curl marca a parte como `application/octet-stream`; o Requests só acrescenta um tipo a partir de uma tupla de 3 itens |
| `-G --data-urlencode "q=kargo takip"` | `params={"q": "kargo takip"}` | Mesma consulta: `?q=kargo+takip` |
| `-X PUT` | `requests.put(url, ...)` | O mesmo vale para `PATCH` e `DELETE` |
| `-H "Name: value"` | `headers={"Name": "value"}` | Os valores precisam ser strings; um `int` gera `InvalidHeader` |
| `-A "ShipmentSync/1.0"` | `headers={"User-Agent": "ShipmentSync/1.0"}` | Sem isso, cada ferramenta envia o próprio nome |
| `-b "session=abc123"` | `cookies={"session": "abc123"}` | Mesmo cabeçalho `Cookie` |
| `-u user:pass` | `auth=("user", "pass")` | Mesmo cabeçalho `Basic` |
| `-L` | Padrão | O cURL precisa de `-L`; `allow_redirects=False` desliga isso no Requests |
| `--max-redirs 5` | `session.max_redirects = 5` | Padrão do Requests: 30 |
| `--connect-timeout 3 -m 20` | `timeout=(3.05, 20)` | `-m` limita a transferência inteira; o tempo limite de leitura é o intervalo entre bytes |
| `-k` / `--cacert ca.pem` | `verify=False` / `verify="ca.pem"` | `verify=False` só em testes locais |
| `-x http://user:pass@pr.proxynet.io:8000` | `proxies={"http": url, "https": url}` | Veja [cURL com proxy](/pt-br/blog/curl-proxy) |
| `--compressed` | Nada | O Requests trata a compressão sozinho |
| `-I` | `requests.head(url)` | O `HEAD` não segue redirecionamentos |
| `-i` / `-v` | `r.headers` / `r.request.headers` | O que foi recebido e o que foi enviado |

Cada opção está descrita no [manual do curl](https://curl.se/docs/manpage.html). Usar um proxy novo a cada requisição é outra tarefa ([Como rotacionar proxies em Python](/pt-br/blog/how-to-rotate-proxies-in-python)).

## Como passar parâmetros de consulta e cabeçalhos?

Passe os valores de consulta para `params=` como um dict:

```python
import requests

params = {"q": "kargo takip", "status": ["pending", "shipped"], "sort": None}
r = requests.get("https://httpbin.org/get", params=params, timeout=20)
print(r.url)  # https://httpbin.org/get?q=kargo+takip&status=pending&status=shipped
```

Uma lista repete a chave, `None` fica de fora, e espaços e caracteres não ASCII são codificados para você. Uma query string que já esteja na URL permanece, e `params=` é acrescentado depois dela. Como percorrer páginas de resultados é explicado em [paginação no web scraping](/pt-br/blog/pagination-web-scraping).

Os cabeçalhos vão num dict de strings, com o token lido do ambiente:

```python
import os
import requests

headers = {
    "Authorization": f"Bearer {os.environ['API_TOKEN']}",
    "Accept": "application/json",
}
r = requests.get("https://httpbin.org/headers", headers=headers, timeout=20)
print(r.json()["headers"]["Authorization"])  # Bearer <seu token>

r = requests.get("https://httpbin.org/basic-auth/user/pass", auth=("user", "pass"), timeout=20)
print(r.status_code)  # 200
```

Os cabeçalhos que toda chamada precisa ficam uma vez só numa `requests.Session`, em `session.headers`. Três regras da documentação do Requests, todas confirmadas nos nossos testes:

- **O `.netrc` vence `headers=`.** Uma entrada do `.netrc` para o host substituiu o nosso cabeçalho `Bearer` por um `Basic`; `auth=` vence os dois.
- **O cabeçalho Authorization fica no host original.** Depois de um redirecionamento de `127.0.0.1` para `localhost`, o cabeçalho tinha sumido.
- **Os valores são strings.** `headers={"X-Page": 2}` gerou `InvalidHeader`.

Sem um `User-Agent` próprio, o Requests envia `python-requests/2.34.2`. O que colocar ali está em [O que é User-Agent?](/pt-br/blog/what-is-user-agent), e por que os cabeçalhos de um mesmo cliente devem ser coerentes entre si, em [Como fazer web scraping sem ser bloqueado](/pt-br/blog/web-scraping-without-getting-blocked).

## Como ler a resposta e capturar erros HTTP?

Um `Response` oferece `r.status_code`, `r.headers` (um dict que não diferencia maiúsculas de minúsculas), `r.content` (bytes brutos), `r.text` (os bytes decodificados com `r.encoding`) e `r.json()`. Caracteres quebrados em `r.text` indicam um palpite errado de codificação ([Erros de codificação no Python](/pt-br/blog/python-unicode-encoding-errors)).

A documentação do Requests avisa que um `r.json()` bem-sucedido não significa uma requisição bem-sucedida: um servidor pode enviar um corpo de erro em JSON junto com um `500`. Confira o status primeiro:

```python
import requests

r = requests.get("https://httpbin.org/status/404", timeout=20)
try:
    r.raise_for_status()
except requests.HTTPError as exc:
    print(exc)  # 404 Client Error: NOT FOUND for url: https://httpbin.org/status/404
```

`raise_for_status()` gera `HTTPError` para qualquer `4xx` ou `5xx`. Quando o corpo está vazio ou é HTML, `r.json()` falha com `JSONDecodeError` ([JSONDecodeError: Expecting Value](/pt-br/blog/jsondecodeerror-expecting-value) lista as causas). Os códigos que vale a pena tentar de novo estão em [Códigos de status HTTP no web scraping](/pt-br/blog/http-status-codes-web-scraping).

## Como enviar e baixar arquivos com o Requests?

`files=` monta um corpo `multipart/form-data`. Uma tupla de 3 itens define o nome do arquivo e o tipo da parte, e os campos de `data=` viajam como partes extras. Como `json=` é ignorado ao lado de `files=`, envie o JSON como uma parte própria:

```python
import json
import requests

with open("report.csv", "rb") as f:
    files = {
        "file": ("report.csv", f, "text/csv"),
        "meta": (None, json.dumps({"source": "warehouse"}), "application/json"),
    }
    r = requests.post("https://httpbin.org/post", files=files, data={"note": "daily"}, timeout=20)

print(r.json()["files"])  # {'file': 'sku,price\n1001,19.90\n'}
print(r.json()["form"])   # {'meta': '{"source": "warehouse"}', 'note': 'daily'}
```

Abra o arquivo em modo binário (`"rb"`): a documentação explica que o Requests pode definir `Content-Length` pelo número de bytes do arquivo, e o modo texto pode deixar esse valor errado. Para uploads muito grandes, a mesma página indica o pacote `requests-toolbelt`, que envia o corpo em streaming.

Para downloads, `stream=True` mantém um corpo grande fora da memória:

```python
import requests

url = "https://example.com/export.csv"  # troque pela URL do seu arquivo
with requests.get(url, stream=True, timeout=(3.05, 60)) as r:
    r.raise_for_status()
    with open("export.csv", "wb") as f:
        for chunk in r.iter_content(chunk_size=64 * 1024):
            f.write(chunk)
```

O download de muitos arquivos de uma mesma página é explicado em [Como baixar todas as imagens de um site](/pt-br/blog/download-all-images-from-website).

## Por que uma requisição que funciona no cURL recebe outra resposta no Requests?

Em geral, as duas requisições não são iguais. Confira nesta ordem:

1. **Redirecionamentos.** O cURL para em um `3xx` sem `-L`; o Requests segue o redirecionamento em todo método, exceto `HEAD`. Quando qualquer uma das ferramentas segue, um POST respondido com `301`, `302` ou `303` vira um GET sem corpo, e `307` ou `308` mantêm o POST, conforme a [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110.html#section-15.4). Uma exceção: com `-X POST` e `-L`, o cURL enviou um POST sem corpo depois de um `302`; `--follow` (curl 8.16.0+) muda para GET. Olhe `r.history`, ou passe `allow_redirects=False` e leia `Location`.
2. **Cabeçalhos padrão.** O curl 8.21.0 enviou `User-Agent: curl/8.21.0`, `Accept: */*` e nenhum `Accept-Encoding`. O Requests enviou `python-requests/2.34.2`, `Accept: */*`, `Connection: keep-alive` e `Accept-Encoding: gzip, deflate` (mais `br` com o brotli instalado, e `zstd` no Python 3.14). Compare com `r.request.headers`.
3. **Versão do HTTP.** O Requests fala só HTTP/1.1: `r.raw.version` devolveu `11` para um site HTTPS. O curl negocia HTTP/2 para HTTPS por padrão quando a sua build tem suporte (`curl -V` lista `HTTP2`), e `-w "%{http_version}"` mostra a versão usada. A nossa build do Windows não tem esse suporte e usou `1.1`.
4. **Ambiente.** Com `Session.trust_env` no padrão `True`, o Requests lê `HTTP_PROXY`, `HTTPS_PROXY` e `NO_PROXY`, as configurações de proxy do sistema no Windows e no macOS quando nenhuma variável está definida, e o `.netrc`. O cURL lê as variáveis (`http_proxy` só em minúsculas), mas não as configurações do sistema, e só lê o `.netrc` com `--netrc`. `requests.utils.get_environ_proxies(url)` mostra o que o Requests escolheu; as variáveis são explicadas em [Uso de proxy com o wget](/pt-br/blog/wget-proxy).
5. **Certificados.** O Requests usa o pacote de certificados `certifi`; o curl no Windows com Schannel usa o repositório de certificados do Windows. Atrás de um proxy corporativo que inspeciona o TLS, o cURL pode passar enquanto o Requests gera `SSLError` (a correção está em [Max Retries Exceeded With URL](/pt-br/blog/max-retries-exceeded-with-url)).
6. **Bytes do corpo.** `json=` serializa o corpo de novo, e `-d @file` remove as quebras de linha. Calcule o hash dos dois corpos se a API os assinar.
7. **O que passa pela rede.** Um [proxy MITM](/pt-br/blog/mitm-proxy) local mostra as duas requisições lado a lado.

Se tudo coincide e a resposta continua diferente, o site está avaliando o próprio cliente, por exemplo o handshake TLS, que os cabeçalhos não mudam. [Cloudflare scraper](/pt-br/blog/cloudflare-scraper) explica como ler uma resposta assim, e [O que é fingerprint TLS e como funciona o JA3?](/pt-br/blog/tls-fingerprinting), o que o handshake revela. O caminho é a API oficial do site ou a permissão do dono; não tratamos de ferramentas que disfarçam um script de navegador.

## Exemplo completo: um pequeno cliente de API com novas tentativas seguras

O script mantém o token Bearer e os cabeçalhos comuns numa única `Session`, envia `params=` com um GET e `json=` com um POST, e confere cada status. Ele só tenta de novo o GET: o [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods/POST) descreve o POST como não idempotente, então uma repetição pode criar um segundo envio. A [classe `Retry` do urllib3](https://urllib3.readthedocs.io/en/stable/reference/urllib3.util.html) já deixa o POST de fora por padrão; o script deixa isso explícito.

```python
"""Um pequeno cliente de API: GET com params, POST com json=, novas tentativas só no GET."""
import os

import requests
from requests.adapters import HTTPAdapter
from urllib3.util import Retry

API = os.environ.get("API_BASE", "https://api.example.com/v1")
TIMEOUT = (3.05, 20)  # tempo limite de conexão, tempo limite de leitura (segundos)

def make_session():
    session = requests.Session()
    session.headers.update({
        "Authorization": f"Bearer {os.environ['API_TOKEN']}",  # nunca deixe o token fixo no código
        "Accept": "application/json",
        "User-Agent": "ShipmentSync/1.0 (+https://example.com/contact)",
    })
    retry = Retry(
        total=3,
        backoff_factor=0.5,
        status_forcelist=[502, 503, 504],
        allowed_methods=["GET"],  # um POST repetido pode criar o mesmo envio duas vezes
        raise_on_status=False,    # devolve a última resposta; raise_for_status() informa o erro
    )
    adapter = HTTPAdapter(max_retries=retry)
    session.mount("https://", adapter)
    session.mount("http://", adapter)
    proxy = os.environ.get("PROXY_URL")  # opcional, ex.: http://user:pass@pr.proxynet.io:8000
    if proxy:
        session.proxies = {"http": proxy, "https": proxy}
        session.trust_env = False  # senão HTTPS_PROXY ou o proxy do sistema vence session.proxies
    return session

def list_shipments(session, status="pending", page=1):
    r = session.get(f"{API}/shipments", params={"status": status, "page": page}, timeout=TIMEOUT)
    r.raise_for_status()
    return r.json()

def create_shipment(session, recipient, weight_kg):
    body = {"recipient": recipient, "weight_kg": weight_kg}
    r = session.post(f"{API}/shipments", json=body, timeout=TIMEOUT)
    r.raise_for_status()
    return r.status_code, r.headers.get("Location"), r.json()

if __name__ == "__main__":
    with make_session() as s:
        try:
            print(list_shipments(s))
            print(create_shipment(s, "Ayse Demir", 3))
        except requests.HTTPError as exc:
            print("API error:", exc)
```

Contra um servidor local que imita a API, o GET devolveu a lista e o POST devolveu `201` com um cabeçalho `Location`. Quando o servidor respondeu `503`, o GET saiu quatro vezes (com esperas de 0, 1 e 2 segundos entre as tentativas) antes de `raise_for_status()` informar o erro; um POST para o mesmo endereço saiu uma vez só. Por um proxy de teste local definido em `PROXY_URL`, as duas chamadas funcionaram, e uma senha errada deu `407 Proxy Authentication Required`.

O urllib3 também respeita `Retry-After` em um `503` por padrão; o tratamento do `429` está em [Códigos de status HTTP no web scraping](/pt-br/blog/http-status-codes-web-scraping), e muitas chamadas em paralelo, em [Concorrência e paralelismo no web scraping](/pt-br/blog/concurrency-vs-parallelism).

## Casos de uso

- **Dados de produtos ou de preços de uma API** que o próprio site oferece ([extração de dados](/pt-br/data-scraping)).
- **Um endpoint JSON encontrado na aba Rede**, chamado em vez de renderizar a página ([Páginas estáticas e dinâmicas](/pt-br/blog/static-vs-dynamic-pages)).
- **Rastrear as páginas por trás dos resultados de uma API** num ritmo educado ([crawler em Python](/pt-br/blog/python-web-crawler)).
- **Testar o seu próprio webhook ou serviço interno** a partir de um script, e não de uma ferramenta gráfica ([Postman com proxy](/pt-br/blog/postman-proxy)).
- **Conferir a resposta de uma API em outro país** por meio de um [Proxies residenciais](https://proxynet.io/pt-br/residential-proxy) de lá.
- **Uma API que só aceita endereços IP cadastrados**, chamada a partir de uma saída fixa ([IP estático para API](/pt-br/blog/static-ip-for-api-access)).
- **A mesma requisição no Node.js** com fetch ou Axios ([cURL em JavaScript](/pt-br/blog/curl-in-javascript)).

## Erros comuns

- **`data=json.dumps(body)` sem `Content-Type`.** O servidor não tem como saber que é JSON; use `json=body`.
- **`json=` e `data=` na mesma chamada.** O JSON é descartado sem aviso.
- **Nenhum `timeout`.** Um servidor que não responde trava o script.
- **Montar a query string à mão.** Espaços e caracteres não ASCII quebram a URL.
- **`r.json()` antes de conferir o status.** Um `500` com corpo de erro em JSON é lido sem problema.
- **Uploads abertos em modo texto.** Use `"rb"`.
- **Copiar `Host` e `Content-Length`.** O Requests enviou o `Host` copiado sem alteração, então um teste contra outro servidor ainda indicava o antigo. Um `Content-Length` copiado num GET sem corpo fez o nosso servidor esperar até o tempo limite de leitura.
- **Colar comandos do navegador em conversores online.** Eles contêm o seu cookie de sessão e o seu token.
- **`verify=False` em produção.** Isso desliga a verificação de certificados.
- **Repetir POST às cegas.** Uma nova tentativa depois de um tempo limite esgotado pode criar um segundo registro.

## Guia de decisão

| Necessidade | Recomendação |
|---|---|
| Corpo JSON para uma API | `requests.post(url, json=data, timeout=20)`, sem `Content-Type` manual |
| Corpo assinado, byte a byte | `data=` com bytes que você serializou, mais `Content-Type` |
| Formulário simples (não um login) | `data=` com um dict; logins e CSRF no guia de sessões |
| Arquivo com campos extras | `files=` mais `data=`; JSON como parte própria `application/json` |
| Conversão rápida de cURL | A tabela acima, ou o curlconverter na sua máquina |
| Funciona no cURL, mas não no Python | Confira `r.history`, `r.request.headers`, `trust_env` e a versão do HTTP |
| HTTP/2 ou chamadas assíncronas | HTTPX; AIOHTTP só para código assíncrono |

## Perguntas frequentes

### Preciso definir o cabeçalho Content-Type ao enviar JSON com o Requests?

Não com `json=`: o Requests define `Content-Type: application/json` sozinho. Você precisa dele quando passa texto JSON por `data=`, porque uma string em `data=` sai sem esse cabeçalho, e uma API rigorosa então responde `415 Unsupported Media Type` ou `400`.

### Qual é a diferença entre json= e data=json.dumps() no Requests?

Os dois enviam texto JSON, mas só `json=` acrescenta o cabeçalho `Content-Type`. Os bytes também podem ser diferentes: `json=` coloca espaços depois de dois-pontos e vírgulas e escapa caracteres não ASCII. Use `json=` por padrão, e `data=` com os seus próprios bytes quando uma API assina o corpo.

### Como enviar um token Bearer com o Python Requests?

Use `headers={"Authorization": f"Bearer {token}"}`, ou defina o cabeçalho uma vez em `session.headers`. Leia o token de uma variável de ambiente. Se um arquivo `.netrc` tiver credenciais para o mesmo host, o Requests usa essas credenciais no lugar do token; `Session.trust_env = False` desliga esse comportamento.

### É possível converter um comando cURL para Python automaticamente?

Sim. O curlconverter transforma um comando cURL em código Requests e roda na sua própria máquina. Revise a saída: ele não acrescenta tempo limite, e o Requests segue redirecionamentos que o comando cURL não seguia. Mantenha comandos com cookies ou tokens longe de conversores online.

### O Requests segue redirecionamentos, e por que o meu POST vira GET?

O Requests segue redirecionamentos em todo método, exceto `HEAD`. Depois de um `301`, `302` ou `303`, um POST vira GET e perde o corpo, como nos navegadores; depois de um `307` ou `308`, continua sendo POST. `allow_redirects=False` para na primeira resposta.

### O Python Requests tem suporte a HTTP/2?

Não. O Requests fala só HTTP/1.1; no nosso teste, `r.raw.version` devolveu `11` para um site HTTPS. O [HTTPX](https://www.python-httpx.org/http2/) tem suporte a HTTP/2 quando você instala `httpx[http2]` e cria o cliente com `http2=True`; o recurso vem desligado por padrão.

## Em resumo

Para uma API JSON, `requests.post(url, json=data, timeout=20)` resolve o trabalho inteiro: o Requests serializa o corpo e define o cabeçalho. `data=` envia formulários ou bytes exatos, `files=` envia corpos multipart, e `params=` monta a query string. Um comando cURL corresponde a esses argumentos opção por opção; quando as respostas continuam diferentes, confira os redirecionamentos, os cabeçalhos padrão, a versão do HTTP e o ambiente, nessa ordem. Quando uma API precisa ser chamada de um país específico ou de um endereço fixo, compare as opções na nossa página de [serviços de proxy](/pt-br/proxy).
