ProxynetProxynet

POST con JSON en Python Requests: equivalencias de cURL

Publicado:

17 min de lectura

Acar Diveroli
Autor: Acar Diveroli
Cajas de opciones de cURL entran por una cinta en una máquina; a la derecha salen headers=, json= y files=, con json= en azul

La documentación de la API de una empresa de mensajería muestra cómo crear un envío con un 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}'. En la terminal devuelve 201 Created. Lo pasas a Python como requests.post(url, data=json.dumps(body), headers={"Authorization": ...}) y el servidor responde 415 Unsupported Media Type: con una cadena en data=, Requests no envía Content-Type, y la línea que lo definía se quedó en el comando cURL.

Esta guía explica json=, data= y files=, los parámetros de consulta, las cabeceras y los tokens Bearer, cómo leer la respuesta y qué argumento de Requests corresponde a cada opción habitual de cURL. Termina con una lista de comprobación para las peticiones que funcionan en cURL pero no en Python y con un pequeño cliente de API que reintenta de forma segura. Todos los ejemplos se ejecutaron con Python 3.13.9, Requests 2.34.2 y curl 8.21.0 (Windows 11), contra httpbin.org y un servidor local que devuelve los bytes que recibe.

¿Cómo se envía una petición POST con Python Requests?

Instala el paquete con pip install requests. PyPI indica como versión actual la 2.34.2, publicada el 14 de mayo de 2026; requiere Python 3.10 o posterior. Cómo elegir entre librerías se explica en HTTPX, Requests y AIOHTTP: comparativa.

Un POST con cuerpo JSON es una sola llamada. httpbin.org/post devuelve lo que recibió:

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"])                      # el mismo cuerpo, convertido de nuevo en dict

El campo data es el cuerpo tal como se envió, con un espacio después de cada dos puntos y de cada coma. requests.put(), requests.patch() y requests.delete() aceptan los mismos argumentos. Pasa siempre timeout: Requests no tiene valor por defecto, así que un servidor que no responde deja tu script esperando (Max Retries Exceeded With URL trata los errores de tiempo de espera).

¿Qué diferencia hay entre json=, data= y files=?

Cada argumento construye el cuerpo de otra forma y pone un Content-Type distinto. Enviamos el mismo dict de cuatro maneras:

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

Lo que muestran las cuatro líneas:

  • json= serializa el dict y pone Content-Type: application/json. Úsalo con las API JSON.
  • data= con un dict envía un formulario, y todos los valores se convierten en texto: weight_kg llegó como '3'.
  • data= con una cadena no envía Content-Type. httpbin lo interpretó igualmente; una API estricta responde 415 o 400. Pon tú la cabecera.
  • json= junto con data= o files= pierde el JSON sin dar ningún error. La guía rápida de Requests indica que el parámetro json se ignora si se pasa data o files.

Envía texto JSON por data= solo cuando importan los bytes exactos. Una API que firma el cuerpo con un HMAC comprueba los bytes que recibe, y json= añade espacios y escapa los caracteres no ASCII ("İzmir" sale como "\u0130zmir"). Construye tú los bytes:

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}

Estos bytes dieron el mismo hash SHA-256 que el cuerpo que envió curl --data-binary @body.json a partir del mismo archivo UTF-8. Los inicios de sesión con formulario y token CSRF se tratan en Sesiones y cookies en Python, y files=, más abajo.

¿Cómo se traduce un comando cURL a Requests, paso a paso?

La misma petición, copiada desde la pestaña Red del navegador en el panel web de la empresa de mensajería, trae algunas líneas más. Cómo encontrar una petición así se explica en Páginas estáticas y dinámicas en web scraping.

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. Deshaz la sintaxis del shell. Quita los saltos de línea con \ (^ en una copia hecha para cmd de Windows) y las comillas.
  2. Pasa la cadena de consulta a params=. ?notify=false se convierte en params={"notify": "false"}.
  3. Filtra las cabeceras. Conserva lo que necesita la API: Authorization, Accept, claves de API. Quita Accept-Encoding, que ya gestiona Requests, y Content-Type cuando usas json=. No copies nunca Host ni Content-Length; Requests los calcula. Las líneas propias del navegador, como sec-fetch-*, rara vez hacen falta, y una línea Cookie va en cookies= o en una Session.
  4. Elige el argumento del cuerpo. El JSON va a json= (o a data= con bytes exactos si el cuerpo va firmado), -d "a=1&b=2" pasa a data={"a": "1", "b": "2"} y -F pasa a files=.
  5. Elige el método. -d, --data-raw, --json y -F significan POST salvo que -X indique otra cosa; -G convierte los datos en una consulta GET.
  6. Añade un timeout= y llama a r.raise_for_status().
  7. Compara. Envía el comando cURL y tu llamada de Python a https://httpbin.org/anything y compara el método, la URL, las cabeceras y el cuerpo que te devuelve.

El resultado, con el token leído de una variable de entorno:

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 hace los pasos 1 a 5 y genera Python Requests como salida por defecto. Instálalo con npm y sustituye curl por curlconverter en el comando. Con el comando anterior, la versión 4.12.0 pasó notify a params, la cookie a cookies= y el cuerpo a json=, y dejó comentadas Content-Type y Accept-Encoding. No añadió ningún timeout, y su README advierte de que el código generado sigue las redirecciones salvo que el comando defina una política de redirección. Un comando copiado del navegador contiene tu cookie de sesión activa y tu token, así que conviértelo en tu propio equipo, no en un sitio web.

¿Qué argumento de Requests corresponde a cada opción de cURL?

Opción de cURLRequestsQué cambia
-d '{"a":1}' con Content-Type: application/jsonjson={"a": 1}Requests añade espacios; usa data= para bytes exactos
--json '{"a":1}'json={"a": 1}, headers={"Accept": "application/json"}--json (curl 7.82.0+) también pone Accept
-d "a=1&b=2"data={"a": "1", "b": "2"}Mismos bytes
--data-binary @body.jsondata=open("body.json", "rb")-d @file quitaría los saltos de línea; estas dos formas los conservan
-F "file=@report.csv"files={"file": open("report.csv", "rb")}curl marca la parte como application/octet-stream; Requests solo añade un tipo si usas una tupla de 3
-G --data-urlencode "q=kargo takip"params={"q": "kargo takip"}Misma consulta: ?q=kargo+takip
-X PUTrequests.put(url, ...)Igual con PATCH y DELETE
-H "Name: value"headers={"Name": "value"}Los valores deben ser cadenas; un int lanza InvalidHeader
-A "ShipmentSync/1.0"headers={"User-Agent": "ShipmentSync/1.0"}Si no, cada herramienta envía su propio nombre
-b "session=abc123"cookies={"session": "abc123"}Misma cabecera Cookie
-u user:passauth=("user", "pass")Misma cabecera Basic
-LPor defectocURL necesita -L; en Requests, allow_redirects=False lo desactiva
--max-redirs 5session.max_redirects = 5Valor por defecto en Requests: 30
--connect-timeout 3 -m 20timeout=(3.05, 20)-m limita toda la transferencia; el timeout de lectura es la pausa máxima entre bytes
-k / --cacert ca.pemverify=False / verify="ca.pem"verify=False solo en pruebas locales
-x http://user:pass@pr.proxynet.io:8000proxies={"http": url, "https": url}Ver cURL con proxy
--compressedNadaRequests gestiona la compresión por sí mismo
-Irequests.head(url)Con HEAD no se siguen redirecciones
-i / -vr.headers / r.request.headersLo que se recibió y lo que se envió

Cada opción se describe en el manual de curl. Usar un proxy distinto en cada petición es otro trabajo (Cómo rotar proxies en Python).

¿Cómo se pasan los parámetros de consulta y las cabeceras?

Pasa los valores de consulta a params= como 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

Una lista repite la clave, None se omite, y los espacios y los caracteres no ASCII se codifican solos. Si la URL ya trae una cadena de consulta, se conserva y params= se añade detrás. Cómo recorrer páginas de resultados se explica en la guía de paginación en web scraping.

Las cabeceras van en un dict de cadenas, con el token leído del entorno:

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 <tu token>

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

Las cabeceras que necesita cada llamada se ponen una sola vez en una requests.Session, mediante session.headers. Tres reglas de la documentación de Requests, todas confirmadas en nuestras pruebas:

  • .netrc gana a headers=. Una entrada de .netrc para el host sustituyó nuestra cabecera Bearer por una Basic; auth= gana a las dos.
  • Authorization se queda en el host original. Tras una redirección de 127.0.0.1 a localhost, la cabecera había desaparecido.
  • Los valores son cadenas. headers={"X-Page": 2} lanzó InvalidHeader.

Si no defines tu propio User-Agent, Requests envía python-requests/2.34.2. Qué poner ahí se explica en ¿Qué es el User-Agent?, y por qué las cabeceras de un mismo cliente deben ser coherentes, en Cómo hacer web scraping sin que te bloqueen.

¿Cómo se lee la respuesta y se capturan los errores HTTP?

Un Response te da r.status_code, r.headers (un dict que no distingue entre mayúsculas y minúsculas), r.content (los bytes en bruto), r.text (los bytes decodificados con r.encoding) y r.json(). Si ves caracteres rotos en r.text, la codificación se adivinó mal (Codificación en Python).

La documentación de Requests advierte de que un r.json() que funciona no significa que la petición haya ido bien: un servidor puede enviar un cuerpo de error en JSON junto con un 500. Comprueba primero el estado:

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() lanza HTTPError con cualquier 4xx o 5xx. Cuando el cuerpo está vacío o es HTML, r.json() falla con JSONDecodeError (Cómo solucionar JSONDecodeError: Expecting Value reúne las causas). Qué códigos conviene reintentar se explica en Códigos de estado HTTP en web scraping.

¿Cómo se suben y descargan archivos con Requests?

files= construye un cuerpo multipart/form-data. Una tupla de 3 fija el nombre del archivo y el tipo de la parte, y los campos de data= viajan como partes adicionales. Como json= se ignora junto a files=, envía el JSON como una parte propia:

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

Abre el archivo en modo binario ("rb"): la documentación explica que Requests puede poner en Content-Length el número de bytes del archivo, y el modo texto puede hacer que ese valor sea incorrecto. Para subidas muy grandes, la misma página remite al paquete requests-toolbelt, que envía el cuerpo en streaming.

Para las descargas, stream=True evita cargar en memoria un cuerpo grande:

python
import requests

url = "https://example.com/export.csv"  # sustitúyela por la URL de tu archivo
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)

Descargar muchos archivos de una misma página se explica en Cómo descargar todas las imágenes de una web.

¿Por qué una petición que funciona en cURL recibe otra respuesta en Requests?

Normalmente, porque las dos peticiones no son iguales. Revisa en este orden:

  1. Redirecciones. Sin -L, cURL se detiene en un 3xx; Requests lo sigue con todos los métodos salvo HEAD. Cuando cualquiera de las dos herramientas sigue la redirección, un POST que recibe 301, 302 o 303 se convierte en un GET sin cuerpo, y con 307 o 308 sigue siendo POST, de acuerdo con RFC 9110. Hay una excepción: con -X POST y -L, cURL envió un POST sin el cuerpo tras un 302; --follow (curl 8.16.0+) cambia a GET. Mira r.history, o pasa allow_redirects=False y lee Location.
  2. Cabeceras por defecto. curl 8.21.0 envió User-Agent: curl/8.21.0, Accept: */* y ningún Accept-Encoding. Requests envió python-requests/2.34.2, Accept: */*, Connection: keep-alive y Accept-Encoding: gzip, deflate (más br con brotli instalado y zstd en Python 3.14). Compara con r.request.headers.
  3. Versión de HTTP. Requests solo habla HTTP/1.1: r.raw.version devolvió 11 en un sitio HTTPS. curl negocia HTTP/2 por defecto con HTTPS cuando su compilación lo admite (curl -V muestra HTTP2), y -w "%{http_version}" imprime la versión usada. Nuestra compilación de Windows no lo incluye y usó 1.1.
  4. Entorno. Con Session.trust_env en su valor por defecto, True, Requests lee HTTP_PROXY, HTTPS_PROXY y NO_PROXY, la configuración de proxy del sistema en Windows y macOS cuando no hay ninguna variable, y .netrc. cURL lee las variables (http_proxy solo en minúsculas), pero no la configuración del sistema, y .netrc solo con --netrc. requests.utils.get_environ_proxies(url) muestra qué eligió Requests; las variables se explican en Proxy con wget.
  5. Certificados. Requests usa el paquete certifi; curl en Windows con Schannel usa el almacén de Windows. Detrás de un proxy corporativo que inspecciona TLS, cURL puede pasar mientras Requests lanza SSLError (la solución está en Max Retries Exceeded With URL).
  6. Bytes del cuerpo. json= vuelve a serializar el cuerpo, y -d @file quita los saltos de línea. Calcula el hash de los dos cuerpos si la API los firma.
  7. Lo que pasa por la red. Un proxy MITM local muestra las dos peticiones una al lado de la otra.

Si todo coincide y la respuesta sigue siendo distinta, el sitio está juzgando al propio cliente, por ejemplo su handshake TLS, que las cabeceras no cambian. Cloudflare y scraping explica cómo leer una respuesta así, y Huella TLS y JA3, qué revela el handshake. El camino es la API oficial del sitio o el permiso de su propietario; no tratamos herramientas que hacen pasar un script por un navegador.

Ejemplo completo: un pequeño cliente de API con reintentos seguros

El script guarda el token Bearer y las cabeceras comunes en una sola Session, envía params= con un GET y json= con un POST, y comprueba cada estado. Solo reintenta el GET: MDN describe POST como no idempotente, así que repetirlo puede crear un segundo envío. La clase Retry de urllib3 ya deja fuera POST por defecto; el script lo indica de forma explícita.

python
"""Un pequeño cliente de API: GET con params, POST con json=, reintentos solo para 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)  # timeout de conexión, timeout de lectura (segundos)


def make_session():
    session = requests.Session()
    session.headers.update({
        "Authorization": f"Bearer {os.environ['API_TOKEN']}",  # nunca escribas el token en el 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"],  # un POST repetido podría crear el mismo envío dos veces
        raise_on_status=False,    # devuelve la última respuesta; raise_for_status() la notifica
    )
    adapter = HTTPAdapter(max_retries=retry)
    session.mount("https://", adapter)
    session.mount("http://", adapter)
    proxy = os.environ.get("PROXY_URL")  # opcional, p. ej. http://user:pass@pr.proxynet.io:8000
    if proxy:
        session.proxies = {"http": proxy, "https": proxy}
        session.trust_env = False  # si no, HTTPS_PROXY o el proxy del sistema ganan a 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 un servidor local que imita la API, el GET devolvió la lista y el POST devolvió 201 con una cabecera Location. Cuando el servidor respondió 503, el GET salió cuatro veces (con esperas de 0, 1 y 2 segundos entre intentos) antes de que raise_for_status() lo notificara; un POST a la misma dirección salió una sola vez. A través de un proxy de prueba local definido en PROXY_URL, las dos llamadas funcionaron, y una contraseña incorrecta dio 407 Proxy Authentication Required.

urllib3 también respeta Retry-After en un 503 por defecto; cómo tratar un 429 se explica en Códigos de estado HTTP en web scraping, y cómo lanzar muchas llamadas en paralelo, en Concurrencia y paralelismo en web scraping.

Casos de uso

Errores comunes

  • data=json.dumps(body) sin Content-Type. El servidor no sabe que es JSON; usa json=body.
  • json= y data= en la misma llamada. El JSON se descarta sin avisar.
  • Sin timeout. Un solo servidor que no responde detiene el script.
  • Montar la cadena de consulta a mano. Los espacios y los caracteres no ASCII rompen la URL.
  • r.json() antes de comprobar el estado. Un 500 con un cuerpo de error en JSON se interpreta sin problema.
  • Subidas abiertas en modo texto. Usa "rb".
  • Copiar Host y Content-Length. Requests envió sin cambios un Host copiado, así que una prueba contra otro servidor seguía nombrando el anterior. Un Content-Length copiado en un GET sin cuerpo hizo que nuestro servidor esperara hasta el timeout de lectura.
  • Comandos del navegador pegados en conversores en línea. Contienen tu cookie de sesión y tu token.
  • verify=False en producción. Desactiva la comprobación de certificados.
  • Reintentar POST a ciegas. Un reintento tras un timeout puede crear un segundo registro.

Guía de decisión

NecesidadRecomendación
Cuerpo JSON para una APIrequests.post(url, json=data, timeout=20), sin Content-Type manual
Cuerpo firmado, byte a bytedata= con bytes que serializaste tú, más Content-Type
Formulario simple (no un inicio de sesión)data= con un dict; inicios de sesión y CSRF en la guía de sesiones
Archivo con campos adicionalesfiles= más data=; el JSON como parte propia application/json
Traducción rápida de cURLLa tabla anterior, o curlconverter en tu equipo
Funciona en cURL, no en PythonRevisa r.history, r.request.headers, trust_env y la versión de HTTP
HTTP/2 o llamadas asíncronasHTTPX; AIOHTTP solo para código asíncrono

Preguntas frecuentes

¿Hay que definir la cabecera Content-Type al enviar JSON por POST con Requests?

Con json=, no: Requests pone él mismo Content-Type: application/json. La necesitas cuando pasas texto JSON por data=, porque una cadena en data= sale sin ella, y entonces una API estricta responde 415 Unsupported Media Type o 400.

¿Qué diferencia hay entre json= y data=json.dumps() en Requests?

Los dos envían texto JSON, pero solo json= añade la cabecera Content-Type. Los bytes también pueden ser distintos: json= escribe espacios después de los dos puntos y las comas y escapa los caracteres no ASCII. Usa json= por defecto, y data= con tus propios bytes cuando una API firma el cuerpo.

¿Cómo se envía un token Bearer con Python Requests?

Usa headers={"Authorization": f"Bearer {token}"}, o defínelo una sola vez en session.headers. Lee el token de una variable de entorno. Si un archivo .netrc tiene credenciales para el mismo host, Requests usa esas en su lugar; Session.trust_env = False lo desactiva.

¿Se puede convertir un comando cURL a Python automáticamente?

Sí. curlconverter convierte un comando cURL en código de Requests y se ejecuta en tu propio equipo. Revisa lo que genera: no añade timeout, y Requests sigue redirecciones que el comando cURL no seguía. No pegues comandos con cookies o tokens en conversores en línea.

¿Requests sigue las redirecciones? ¿Por qué mi POST se convierte en GET?

Requests sigue las redirecciones con todos los métodos salvo HEAD. Tras un 301, 302 o 303, un POST pasa a ser un GET y pierde su cuerpo, igual que en los navegadores; tras un 307 o 308, sigue siendo POST. allow_redirects=False se detiene en la primera respuesta.

¿Python Requests admite HTTP/2?

No. Requests solo habla HTTP/1.1; en nuestra prueba, r.raw.version devolvió 11 en un sitio HTTPS. HTTPX admite HTTP/2 si instalas httpx[http2] y creas el cliente con http2=True; viene desactivado por defecto.

En resumen

Para una API JSON, requests.post(url, json=data, timeout=20) es todo el trabajo: Requests serializa el cuerpo y pone la cabecera. data= envía formularios o bytes exactos, files= cuerpos multipart y params= la cadena de consulta. Un comando cURL se traduce a estos argumentos opción por opción; si las respuestas siguen siendo distintas, revisa las redirecciones, las cabeceras por defecto, la versión de HTTP y el entorno, en ese orden. Si tienes que llamar a una API desde un país concreto o desde una dirección fija, compara las opciones en nuestra página de servicios de proxy.