---
title: "Python Requests ile POST ve JSON: cURL Karşılıkları"
description: "Python Requests ile JSON POST etmek için requests.post(url, json=data) yazın; Content-Type header'ını Requests ekler. data=, params= ve cURL karşılıkları."
url: https://proxynet.io/tr/blog/python-requests-post-json
date: 2026-09-25
author: "Acar Diveroli"
category: "Nasıl Yapılır, Web Scraping"
lang: tr
---

# Python Requests ile POST ve JSON: cURL Karşılıkları

Bir kargo şirketinin API belgeleri, gönderi oluşturmayı bir cURL komutuyla gösteriyor: `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}'`. Komut terminalde `201 Created` döndürüyor. Aynı isteği Python'a `requests.post(url, data=json.dumps(body), headers={"Authorization": ...})` diye taşıdığınızda sunucu `415 Unsupported Media Type` dönüyor. Sebebi şu: `data=` içine bir dize (string) verildiğinde Requests `Content-Type` header'ını göndermez. Header'lar isteğin başında giden ad-değer satırlarıdır; `Content-Type` sunucuya gövdedeki (body) verinin türünü bildirir. Onu ayarlayan `-H` satırı ise cURL komutunda kaldı.

Bu yazıda `json=`, `data=` ve `files=` argümanlarını, sorgu parametrelerini, header'ları, Bearer token'ı, yanıtın okunmasını ve sık kullanılan cURL seçeneklerinin Requests'teki karşılıklarını anlatıyoruz. Yazının sonunda cURL'de çalışıp Python'da çalışmayan istekler için bir kontrol listesi ve istekleri güvenli biçimde yeniden deneyen küçük bir API istemcisi var. Bütün örnekleri Python 3.13.9, Requests 2.34.2 ve curl 8.21.0 (Windows 11) ile, httpbin.org'da ve aldığı baytları olduğu gibi geri yollayan yerel bir sunucuda denedik.

> **Not: Kısa cevap**
>
> Python Requests ile JSON göndermek için `requests.post(url, json=data, timeout=20)` yazın. Requests dict'i JSON'a çevirir ve `Content-Type: application/json` header'ını kendisi ekler. `data=` bir dict alırsa form gönderir, bir dize alırsa metni `Content-Type` olmadan yollar; `files=` ise `multipart/form-data` gövdesi kurar. Bir cURL komutunu çevirirken `-H` satırları `headers=`, `-G` ile verilen sorgu değerleri `params=`, `-u` değeri `auth=`, `-F` alanları `files=`, `-x` adresi de `proxies=` argümanına gider. Çevirdiğiniz istek farklı bir cevap alıyorsa önce yönlendirmelere bakın: cURL yönlendirmeyi yalnız `-L` ile izler, Requests ise varsayılan olarak izler.

## Python Requests ile POST isteği nasıl gönderilir?

Paketi `pip install requests` ile kurun. [PyPI](https://pypi.org/project/requests/) güncel sürüm olarak 14 Mayıs 2026'da çıkan 2.34.2'yi gösteriyor; bu sürüm Python 3.10 ya da daha yenisini gerektiriyor. Hangi kütüphaneyi seçeceğinizi [HTTPX, Requests ve AIOHTTP Karşılaştırması](/tr/blog/httpx-vs-requests-vs-aiohttp) yazısında ele aldık.

JSON gövdeli bir POST tek bir çağrıdır. httpbin.org/post aldığı isteği olduğu gibi geri gönderir:

```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"])                      # aynı gövde, dict'e geri çevrilmiş
```

`data` alanı gövdeyi gönderildiği hâliyle gösterir: her `:` ve `,` işaretinden sonra bir boşluk var. `requests.put()`, `requests.patch()` ve `requests.delete()` aynı argümanları alır. `timeout` değerini, yani zaman aşımını her zaman verin. Requests'te varsayılan zaman aşımı yoktur; cevap vermeyen bir sunucu betiğinizi süresiz bekletir (zaman aşımı hatalarını [Max Retries Exceeded With URL](/tr/blog/max-retries-exceeded-with-url) yazısında anlattık).

## json=, data= ve files= arasındaki fark nedir?

Her argüman gövdeyi kendi biçiminde kurar ve kendi `Content-Type` değerini koyar. Aynı dict'i dört yolla gönderdik:

```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
```

Dört satır şunu gösteriyor:

- **`json=`** dict'i JSON metnine çevirir (serileştirir) ve `Content-Type: application/json` header'ını ekler. JSON API'leri için bunu kullanın.
- **Dict verilen `data=`** form gönderir ve her değer metne dönüşür: `weight_kg` sunucuya `'3'` olarak ulaştı.
- **Dize verilen `data=`** `Content-Type` göndermez. httpbin gövdeyi yine de ayrıştırdı; header'ı kontrol eden bir API ise `415` ya da `400` döner. Header'ı kendiniz ekleyin.
- **`json=` ile birlikte `data=` ya da `files=`** verilirse JSON hata vermeden kaybolur. [Requests hızlı başlangıç belgesi](https://requests.readthedocs.io/en/latest/user/quickstart/), `data` ya da `files` verildiğinde `json` parametresinin yok sayıldığını yazıyor.

`data=` ile JSON metni göndermeyi, baytların birebir aynı kalması gereken durumlara saklayın. Gövdeyi HMAC ile imzalayan bir API aldığı baytları kontrol eder; `json=` ise araya boşluk koyar ve ASCII dışı karakterleri kaçış dizisine çevirir (`"İzmir"`, `"\u0130zmir"` olarak gider). Böyle bir API için baytları kendiniz üretin:

```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}
```

Bu baytların SHA-256 hash'i, aynı UTF-8 dosyasından `curl --data-binary @body.json` ile gönderilen gövdenin hash'iyle aynı çıktı. CSRF token'lı giriş formlarını [Python'da Oturum ve Çerez Yönetimi](/tr/blog/python-login-session-cookies) yazısında anlattık; `files=` argümanı aşağıda.

## cURL komutu Python Requests'e adım adım nasıl çevrilir?

Aynı isteği kargo şirketinin web panelinde, tarayıcının geliştirici araçlarındaki Network sekmesinden kopyaladığınızda birkaç satır fazlası gelir. Böyle bir isteğin nasıl bulunacağını [Statik ve Dinamik Sayfalar](/tr/blog/static-vs-dynamic-pages) yazısında anlattık.

```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. **Kabuk (shell) işaretlerini temizleyin.** Satırları bölen `\` işaretlerini (Windows `cmd` için alınmış kopyada `^`) ve tırnakları kaldırın.
2. **Sorgu dizesini `params=` içine taşıyın.** `?notify=false`, `params={"notify": "false"}` olur.
3. **Header'ları ayıklayın.** API'nin istediklerini bırakın: `Authorization`, `Accept`, API anahtarları. Requests'in kendi yönettiği `Accept-Encoding` satırını, `json=` kullanıyorsanız `Content-Type` satırını da çıkarın. `Host` ve `Content-Length` satırlarını hiçbir zaman kopyalamayın; bunları Requests kendisi hesaplar. `sec-fetch-*` gibi tarayıcı satırlarına nadiren gerek olur. `Cookie` satırının yeri ise `cookies=` argümanı ya da bir `Session` nesnesidir.
4. **Gövde argümanını seçin.** JSON gövde `json=` argümanına gider (gövde imzalanıyorsa birebir baytlarla `data=`). `-d "a=1&b=2"` karşılığı `data={"a": "1", "b": "2"}`, `-F` karşılığı `files=`.
5. **Metodu belirleyin.** `-X` başka bir metot belirtmiyorsa `-d`, `--data-raw`, `--json` ve `-F` POST anlamına gelir; `-G` ise değerleri bir GET sorgusuna çevirir.
6. **Bir `timeout=` ekleyin** ve `r.raise_for_status()` çağırın.
7. **Karşılaştırın.** cURL komutunu ve Python çağrınızı `https://httpbin.org/anything` adresine yollayın; httpbin'in geri gönderdiği metodu, URL'yi, header'ları ve gövdeyi karşılaştırın.

Token'ı ortam değişkeninden okuyan son hâl:

```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"))
```

[curlconverter](https://github.com/curlconverter/curlconverter) 1-5. adımları kendisi yapar; varsayılan çıktı dili Python Requests'tir. npm ile kurun ve komuttaki `curl` yerine `curlconverter` yazın. Yukarıdaki komutu verdiğimizde 4.12.0 sürümü `notify` değerini `params`, çerezi `cookies=`, gövdeyi `json=` içine taşıdı; `Content-Type` ve `Accept-Encoding` satırlarını yoruma aldı. Zaman aşımı eklemedi. README dosyası da bir konuda uyarıyor: komutta yönlendirme ayarı yoksa üretilen kod yönlendirmeleri izler. Tarayıcıdan kopyalanan bir komut oturumunuzun canlı çerezini ve token'ınızı taşır; bu yüzden komutu bir web sitesinde değil, kendi bilgisayarınızda çevirin.

## Hangi cURL seçeneği hangi Requests argümanına karşılık gelir?

| cURL seçeneği | Requests | Fark |
|---|---|---|
| `-d '{"a":1}'` ve `Content-Type: application/json` | `json={"a": 1}` | Requests boşluk ekler; birebir bayt için `data=` |
| `--json '{"a":1}'` | `json={"a": 1}`, `headers={"Accept": "application/json"}` | `--json` (curl 7.82.0+) `Accept` değerini de ayarlar |
| `-d "a=1&b=2"` | `data={"a": "1", "b": "2"}` | Baytlar aynı |
| `--data-binary @body.json` | `data=open("body.json", "rb")` | `-d @file` satır sonlarını silerdi; bu ikisi korur |
| `-F "file=@report.csv"` | `files={"file": open("report.csv", "rb")}` | curl parçayı `application/octet-stream` diye etiketler; Requests türü yalnız üç elemanlı tuple verirseniz ekler |
| `-G --data-urlencode "q=kargo takip"` | `params={"q": "kargo takip"}` | Sorgu aynı: `?q=kargo+takip` |
| `-X PUT` | `requests.put(url, ...)` | `PATCH` ve `DELETE` için de aynı |
| `-H "Name: value"` | `headers={"Name": "value"}` | Değerler dize olmalı; `int` verilirse `InvalidHeader` fırlatılır |
| `-A "ShipmentSync/1.0"` | `headers={"User-Agent": "ShipmentSync/1.0"}` | Verilmezse her araç kendi adını gönderir |
| `-b "session=abc123"` | `cookies={"session": "abc123"}` | `Cookie` header'ı aynı |
| `-u user:pass` | `auth=("user", "pass")` | `Basic` header'ı aynı |
| `-L` | Varsayılan | cURL'de `-L` gerekir; Requests'te `allow_redirects=False` kapatır |
| `--max-redirs 5` | `session.max_redirects = 5` | Requests varsayılanı: 30 |
| `--connect-timeout 3 -m 20` | `timeout=(3.05, 20)` | `-m` aktarımın tamamını sınırlar; okuma zaman aşımı ise iki bayt arasındaki bekleme süresidir |
| `-k` / `--cacert ca.pem` | `verify=False` / `verify="ca.pem"` | `verify=False` yalnız yerel testlerde |
| `-x http://user:pass@pr.proxynet.io:8000` | `proxies={"http": url, "https": url}` | Bkz. [cURL ile Proxy Kullanımı](/tr/blog/curl-proxy) |
| `--compressed` | Gerek yok | Sıkıştırmayı Requests kendisi halleder |
| `-I` | `requests.head(url)` | `HEAD` isteğinde yönlendirme izlenmez |
| `-i` / `-v` | `r.headers` / `r.request.headers` | Alınan ve gönderilen header'lar |

Seçeneklerin hepsi [curl kılavuzunda](https://curl.se/docs/manpage.html) açıklanıyor. Her istekte başka bir proxy kullanmak ise ayrı bir konu ([Python'da Proxy'ler Nasıl Döndürülür?](/tr/blog/rotating-proxy-python)).

## Sorgu parametreleri ve header'lar nasıl gönderilir?

Sorgu değerlerini `params=` argümanına dict olarak verin:

```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
```

Değeri liste olan anahtar tekrarlanır, `None` olan atlanır; boşlukları ve ASCII dışı karakterleri Requests kendisi kodlar. URL'de zaten bir sorgu dizesi varsa korunur ve `params=` onun arkasına eklenir. Sonuçlarda sayfa sayfa ilerlemeyi [Sayfalama (Pagination) Nedir?](/tr/blog/pagination-web-scraping) yazısında anlattık.

Header'lar, değerleri dize olan bir dict'e yazılır. Token yine ortam değişkeninden okunur:

```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 <token'ınız>

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

Her çağrıda gereken header'ları `session.headers` ile bir `requests.Session` üzerinde tek seferde tanımlayın. Requests belgelerinde geçen üç kural var; üçünü de testlerimizde doğruladık:

- **`.netrc`, `headers=` değerini ezer.** Aynı host için tanımlı bir `.netrc` kaydı (sunucu başına kullanıcı adı ve şifre tutan dosya) `Bearer` header'ımızı bir `Basic` header'la değiştirdi. `auth=` ise ikisini de ezer.
- **Authorization header'ı ilk host'ta kalır.** `127.0.0.1` adresinden `localhost` adresine yönlendirilen istekte header artık yoktu.
- **Değerler dize olmalı.** `headers={"X-Page": 2}` bir `InvalidHeader` hatası fırlattı.

Kendi `User-Agent` değerinizi, yani isteği gönderen programın adını bildiren header'ı vermezseniz Requests `python-requests/2.34.2` gönderir. Oraya ne yazılacağını [User-Agent Nedir?](/tr/blog/what-is-user-agent) yazısında bulabilirsiniz. Aynı istemcinin gönderdiği header'lar neden birbiriyle tutarlı olmalı, onu da [Web Scraping'de Engellenmeden Veri Toplama Yöntemleri](/tr/blog/web-scraping-without-getting-blocked) yazısında anlattık.

## Yanıt nasıl okunur, HTTP hataları nasıl yakalanır?

`Response` nesnesinde `r.status_code`, `r.headers` (büyük-küçük harf ayırmayan bir dict), `r.content` (ham baytlar), `r.text` (`r.encoding` ile çözülmüş baytlar) ve `r.json()` bulunur. `r.text` içinde bozuk karakterler görüyorsanız kodlama (encoding) yanlış tahmin edilmiştir ([Python Türkçe Karakter Sorunu](/tr/blog/python-unicode-encoding-errors)).

Requests belgeleri bir konuda uyarıyor: `r.json()` hatasız çalıştı diye istek başarılı olmuş sayılmaz, çünkü sunucu `500` koduyla birlikte JSON biçiminde bir hata gövdesi gönderebilir. Bu yüzden önce durum koduna bakın:

```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()`, her `4xx` ve `5xx` cevabında `HTTPError` fırlatır. Gövde boş ya da HTML olduğunda `r.json()` bir `JSONDecodeError` hatası verir; nedenlerini [JSONDecodeError: Expecting Value](/tr/blog/jsondecodeerror-expecting-value) yazısında sıraladık. Hangi kodlarda yeniden denemek gerektiğini de [Scraping'de HTTP Hata Kodları](/tr/blog/http-status-codes-web-scraping) yazısında anlattık.

## Requests ile dosya nasıl yüklenir ve indirilir?

`files=` bir `multipart/form-data` gövdesi kurar. Üç elemanlı bir tuple dosya adını ve parçanın türünü belirler; `data=` alanları da ayrı parçalar olarak gider. `files=` varken `json=` yok sayıldığı için JSON'u kendi parçası olarak gönderin:

```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'}
```

Dosyayı ikili (binary) kipte, `"rb"` ile açın. Belgelere göre Requests `Content-Length` değerini dosyanın bayt sayısından alabilir; metin kipinde açılan dosyada bu değer yanlış çıkabilir. Çok büyük yüklemeler için aynı sayfa, gövdeyi parça parça (stream) gönderen `requests-toolbelt` paketini öneriyor.

İndirmede `stream=True` verirseniz büyük bir dosya belleğe bütünüyle alınmadan parça parça diske yazılır:

```python
import requests

url = "https://example.com/export.csv"  # kendi dosyanızın adresiyle değiştirin
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)
```

Bir sayfadaki çok sayıda dosyayı indirmeyi [Sayfadaki Tüm Resimleri İndirme](/tr/blog/download-all-images-from-website) yazısında anlattık.

## cURL'de çalışan istek Requests'te neden farklı cevap alıyor?

Çoğu zaman iki istek aslında birbirinden farklıdır. Şu sırayla kontrol edin:

1. **Yönlendirmeler.** cURL, `-L` olmadan bir `3xx` cevabında durur; Requests ise `HEAD` dışındaki her metotta yönlendirmeyi izler. Hangi araç izlerse izlesin, `301`, `302` ya da `303` alan bir POST gövdesiz bir GET'e dönüşür; `307` ve `308` ise POST'u korur. Bu davranış [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110.html#section-15.4) ile uyumlu. Bir istisna var: `-X POST` ve `-L` birlikte verildiğinde cURL `302` sonrasında gövdesiz bir POST gönderdi; `--follow` (curl 8.16.0+) ise GET'e geçer. `r.history` listesine bakın ya da `allow_redirects=False` verip `Location` header'ını okuyun.
2. **Varsayılan header'lar.** curl 8.21.0 `User-Agent: curl/8.21.0` ve `Accept: */*` gönderdi, `Accept-Encoding` göndermedi. Requests ise `python-requests/2.34.2`, `Accept: */*`, `Connection: keep-alive` ve `Accept-Encoding: gzip, deflate` gönderdi (brotli kuruluysa `br`, Python 3.14'te de `zstd` eklenir). `r.request.headers` ile karşılaştırın.
3. **HTTP sürümü.** Requests yalnız HTTP/1.1 kullanır: bir HTTPS sitesinde `r.raw.version` değeri `11` döndü. curl ise derlemesi destekliyorsa HTTPS'te varsayılan olarak HTTP/2 ile bağlanır (`curl -V` çıktısında `HTTP2` geçiyorsa destek vardır); hangi sürümün kullanıldığını `-w "%{http_version}"` yazdırır. Bizim Windows derlememizde bu destek yoktu, bağlantı `1.1` ile kuruldu.
4. **Ortam.** `Session.trust_env` varsayılan değerinde (`True`) kaldıkça Requests şunları okur: `HTTP_PROXY`, `HTTPS_PROXY` ve `NO_PROXY` ortam değişkenleri, bu değişkenler yoksa Windows ve macOS'ta sistem proxy ayarları, bir de `.netrc` dosyası. cURL değişkenleri okur (`http_proxy` yalnız küçük harfle yazılırsa), sistem ayarlarına bakmaz, `.netrc` dosyasını da yalnız `--netrc` ile kullanır. Requests'in hangi proxy'yi seçtiğini `requests.utils.get_environ_proxies(url)` gösterir. Bu değişkenleri [wget ile Proxy Kullanımı](/tr/blog/wget-proxy) yazısında açıkladık.
5. **Sertifikalar.** Requests `certifi` paketindeki sertifika listesine güvenir; Windows'ta Schannel ile derlenmiş curl ise Windows sertifika deposuna bakar. TLS trafiğini denetleyen bir şirket proxy'sinin arkasında cURL sorunsuz bağlanırken Requests `SSLError` verebilir (çözümü [Max Retries Exceeded With URL](/tr/blog/max-retries-exceeded-with-url) yazısında).
6. **Gövde baytları.** `json=` gövdeyi yeniden serileştirir, `-d @file` de satır sonlarını siler. API gövdeyi imzalıyorsa iki gövdenin hash'ini alıp karşılaştırın.
7. **Ağdaki gerçek trafik.** Yerel bir [MITM proxy](/tr/blog/mitm-proxy) iki isteği yan yana gösterir.

Her şey aynı olduğu hâlde cevap yine farklıysa site istemcinin kendisine, örneğin TLS el sıkışmasına (handshake) bakıyordur. Header'ları değiştirmek el sıkışmayı değiştirmez. Böyle bir cevabın nasıl okunacağını [Cloudflare Scraper](/tr/blog/cloudflare-scraper) yazısı, el sıkışmanın neleri ele verdiğini de [TLS Parmak İzi ve JA3](/tr/blog/tls-fingerprinting) yazısı anlatıyor. Buradan sonraki yol sitenin resmi API'si ya da site sahibinin iznidir; bir betiği tarayıcı gibi gösteren araçları anlatmıyoruz.

## Tam örnek: güvenli yeniden deneme yapan küçük bir API istemcisi

Betik, Bearer token'ı ve ortak header'ları tek bir `Session` üzerinde tutar, GET ile `params=`, POST ile `json=` gönderir ve her durum kodunu kontrol eder. Yalnız GET isteğini yeniden dener. [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods/POST) POST'u idempotent olmayan, yani tekrarlandığında aynı sonucu vermeyebilen bir metot olarak tanımlar: ikinci kez giden bir POST ikinci bir gönderi kaydı açabilir. [urllib3 `Retry` sınıfı](https://urllib3.readthedocs.io/en/stable/reference/urllib3.util.html) POST'u zaten varsayılan olarak dışarıda bırakıyor; betik bunu yine de açıkça belirtiyor.

```python
"""Küçük bir API istemcisi: params ile GET, json= ile POST, yeniden deneme yalnız GET için."""
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)  # bağlantı zaman aşımı, okuma zaman aşımı (saniye)

def make_session():
    session = requests.Session()
    session.headers.update({
        "Authorization": f"Bearer {os.environ['API_TOKEN']}",  # token'ı asla koda gömmeyin
        "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"],  # tekrarlanan bir POST aynı gönderiyi iki kez oluşturabilir
        raise_on_status=False,    # son yanıtı geri ver, hatayı raise_for_status() bildirsin
    )
    adapter = HTTPAdapter(max_retries=retry)
    session.mount("https://", adapter)
    session.mount("http://", adapter)
    proxy = os.environ.get("PROXY_URL")  # isteğe bağlı, ör. http://user:pass@pr.proxynet.io:8000
    if proxy:
        session.proxies = {"http": proxy, "https": proxy}
        session.trust_env = False  # yoksa HTTPS_PROXY ya da sistem proxy'si session.proxies'in önüne geçer
    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)
```

API'yi taklit eden yerel bir sunucuyla denediğimizde GET listeyi döndürdü, POST ise `Location` header'ıyla birlikte `201` aldı. Sunucu `503` cevabı verdiğinde GET dört kez gitti (denemeler arasında 0, 1 ve 2 saniye bekleyerek), ardından hatayı `raise_for_status()` bildirdi. Aynı adrese gönderilen POST ise yalnız bir kez gitti. `PROXY_URL` ile yerel bir test proxy'si verdiğimizde iki çağrı da çalıştı; yanlış şifreyle `407 Proxy Authentication Required` geldi.

urllib3, `503` cevabındaki `Retry-After` header'ına da varsayılan olarak uyar. `429` cevabını nasıl ele alacağınızı [Scraping'de HTTP Hata Kodları](/tr/blog/http-status-codes-web-scraping) yazısında, çok sayıda paralel çağrıyı da [Concurrency ve Parallelism](/tr/blog/concurrency-vs-parallelism) yazısında anlattık.

## Kullanım alanları

- **Sitenin sunduğu bir API'den ürün ya da fiyat verisi** almak ([veri kazıma](/tr/data-scraping)).
- **Network sekmesinde bulduğunuz bir JSON endpoint'ini**, sayfayı render etmek yerine doğrudan çağırmak ([Statik ve Dinamik Sayfalar](/tr/blog/static-vs-dynamic-pages)).
- **API sonuçlarının arkasındaki sayfaları** siteyi yormayan bir hızla taramak ([Python ile Web Crawler Nasıl Yazılır?](/tr/blog/python-web-crawler)).
- **Kendi webhook'unuzu ya da iç servisinizi** bir arayüz aracı yerine betikle test etmek ([Postman Proxy Ayarları](/tr/blog/postman-proxy)).
- **Bir API'nin başka bir ülkeye verdiği cevabı** o ülkedeki bir [Residential Proxy](https://proxynet.io/tr/residential-proxy) üzerinden kontrol etmek.
- **Yalnız kayıtlı IP adreslerini kabul eden bir API'yi** tek ve sabit bir çıkış IP'sinden çağırmak ([API için Statik IP](/tr/blog/static-ip-for-api-access)).
- **Aynı isteği Node.js'te** fetch ya da Axios ile göndermek ([JavaScript'te cURL](/tr/blog/curl-in-javascript)).

## Sık yapılan hatalar

- **`Content-Type` olmadan `data=json.dumps(body)`.** Sunucu gövdenin JSON olduğunu anlayamaz; `json=body` kullanın.
- **Aynı çağrıda `json=` ve `data=`.** JSON sessizce düşer.
- **`timeout` vermemek.** Cevap vermeyen tek bir sunucu betiğin tamamını durdurur.
- **Sorgu dizesini elle birleştirmek.** Boşluklar ve ASCII dışı karakterler URL'yi bozar.
- **Durum kontrolünden önce `r.json()` çağırmak.** Gövdesi JSON hata mesajı olan bir `500` de sorunsuz ayrıştırılır.
- **Yüklenecek dosyayı metin kipinde açmak.** `"rb"` kullanın.
- **`Host` ve `Content-Length` satırlarını kopyalamak.** Requests kopyalanan `Host` değerini değiştirmeden gönderdi; bu yüzden başka bir sunucuyla yaptığımız test hâlâ eski sunucunun adını gönderiyordu. Gövdesiz bir GET'e kopyalanan `Content-Length` ise sunucumuzu okuma zaman aşımına kadar bekletti.
- **Tarayıcıdan kopyalanan komutları çevrimiçi dönüştürücülere yapıştırmak.** Bu komutlar oturum çerezinizi ve token'ınızı içerir.
- **Canlı ortamda `verify=False`.** Sertifika kontrolünü kapatır.
- **POST'u körlemesine yeniden denemek.** Zaman aşımından sonra yapılan bir deneme ikinci bir kayıt oluşturabilir.

## Karar rehberi

| İhtiyaç | Öneri |
|---|---|
| Bir API için JSON gövde | `requests.post(url, json=data, timeout=20)`, elle `Content-Type` eklemeden |
| İmzalı, bayt bayt aynı gövde | Kendi serileştirdiğiniz baytlarla `data=`, artı `Content-Type` |
| Düz form (giriş formu değil) | Dict ile `data=`; giriş ve CSRF için oturum rehberi |
| Ek alanlarla birlikte dosya | `files=` artı `data=`; JSON ayrı bir `application/json` parçası olarak |
| Hızlı cURL çevirisi | Yukarıdaki tablo ya da kendi bilgisayarınızda curlconverter |
| cURL'de çalışıyor, Python'da çalışmıyor | `r.history`, `r.request.headers`, `trust_env` ve HTTP sürümünü kontrol edin |
| HTTP/2 ya da async çağrılar | HTTPX; yalnız async için AIOHTTP |

## Sıkça sorulan sorular

### Requests ile JSON gönderirken Content-Type header'ını ayarlamak gerekir mi?

`json=` kullanıyorsanız gerekmez: Requests `Content-Type: application/json` header'ını kendisi ekler. JSON metnini `data=` ile gönderdiğinizde ise gerekir, çünkü `data=` içindeki bir dize bu header olmadan gider ve header'ı kontrol eden bir API `415 Unsupported Media Type` ya da `400` döner.

### Requests'te json= ile data=json.dumps() arasındaki fark nedir?

İkisi de JSON metni gönderir, ama `Content-Type` header'ını yalnız `json=` ekler. Baytlar da farklı olabilir: `json=` iki nokta üst üste ve virgüllerden sonra boşluk bırakır, ASCII dışı karakterleri de kaçış dizisine çevirir. Varsayılan olarak `json=` kullanın; API gövdeyi imzalıyorsa kendi ürettiğiniz baytları `data=` ile gönderin.

### Python Requests ile Bearer token nasıl gönderilir?

`headers={"Authorization": f"Bearer {token}"}` yazın ya da header'ı bir kez `session.headers` içinde ayarlayın. Token'ı bir ortam değişkeninden okuyun. Bir `.netrc` dosyasında aynı host için kimlik bilgisi varsa Requests onu kullanır; bu davranışı `Session.trust_env = False` kapatır.

### cURL komutunu Python'a otomatik çevirebilir miyim?

Evet. curlconverter bir cURL komutunu Requests koduna çevirir ve kendi bilgisayarınızda çalışır. Çıktıyı gözden geçirin: zaman aşımı eklemez, ayrıca Requests, cURL komutunun izlemediği yönlendirmeleri izler. Çerez ya da token içeren komutları çevrimiçi dönüştürücülere vermeyin.

### Requests yönlendirmeleri izler mi, POST neden GET'e dönüşüyor?

Requests, `HEAD` dışındaki her metotta yönlendirmeleri izler. `301`, `302` ya da `303` sonrasında POST, tarayıcılarda olduğu gibi GET'e dönüşür ve gövdesini kaybeder; `307` ya da `308` sonrasında ise POST olarak kalır. `allow_redirects=False` verirseniz Requests ilk cevapta durur.

### Python Requests HTTP/2 destekler mi?

Hayır. Requests yalnız HTTP/1.1 kullanır; testimizde bir HTTPS sitesi için `r.raw.version` değeri `11` döndü. [HTTPX](https://www.python-httpx.org/http2/), `httpx[http2]` kurulduğunda ve istemci `http2=True` ile oluşturulduğunda HTTP/2 destekler; bu özellik varsayılan olarak kapalıdır.

## Özet

Bir JSON API için `requests.post(url, json=data, timeout=20)` yazmak yeterlidir: Requests gövdeyi serileştirir ve header'ı ekler. `data=` form ya da birebir bayt, `files=` multipart gövde, `params=` de sorgu dizesi gönderir. Bir cURL komutunun her seçeneği bu argümanlardan birine karşılık gelir. Çevirdikten sonra cevaplar hâlâ farklıysa sırasıyla yönlendirmelere, varsayılan header'lara, HTTP sürümüne ve ortama bakın. Bir API'yi belirli bir ülkeden ya da sabit bir IP adresinden çağırmanız gerekiyorsa seçenekleri [proxy hizmetleri](/tr/proxy) sayfamızda karşılaştırabilirsiniz.
