Geonode logo
Geonode Team

Geonode Team

Actualizado: 7 de octubre de 2026

Publicado: 2 de septiembre de 2026

Cómo enviar una solicitud POST con curl (+ejemplos)

`curl -d "key=value" https://example.com` envía una solicitud POST. Esa es, básicamente, toda la respuesta, y también es la causa del error más habitual con el que se topan los usuarios a continuación. `-d` establece un tipo de contenido «form-encoded». Si envías datos en formato JSON con este tipo de contenido y te olvidas del encabezado, muchas API te rechazarán con un código de error 415. Esta guía aborda los datos de formulario, el JSON —incluido el atajo que la mayoría de la gente desconoce—, la subida de archivos y cómo determinar cuál de las diversas opciones de datos es la que realmente te conviene.

La información, que seré breve ya que apenas viene al caso: somos Geonode y vendemos proxies. Una solicitud POST no requiere nada por nuestra parte. Todo lo que se explica a continuación funciona desde tu propia conexión hacia tus propios puntos finales. Los proxies solo entran en escena mucho más adelante, y hay una breve nota al final sobre lo único que realmente cambia cuando envías una solicitud POST a través de uno de ellos —que no es lo que la mayoría de la gente espera—.

Conceptos básicos

curl -d "name=Ada&role=engineer" https://api.example.com/users

El manual de curl describe -d, --data

: «Envía los datos especificados a un servidor. Para HTTP(S), esto se realiza con el método POST, del mismo modo que lo hace un navegador cuando un usuario ha rellenado un formulario HTML y pulsa el botón de envío. Esta opción hace que curl pase los datos al servidor utilizando el tipo de contenido application/x-www-form-urlencoded

».

Hay dos cosas que ocurren automáticamente y ambas son importantes.

**-d

implica POST.** No es necesario utilizar -X POST

, y añadirlo no cambia nada, salvo en la gestión de redireccionamientos, donde se aplica -X

en cada salto.

**-d

establece Content-Type: application/x-www-form-urlencoded

.** Esto es correcto para el envío de formularios, pero incorrecto para casi todo lo demás.

Puedes repetir -d

y curl unirá las partes: el manual señala que «utilizar -d name=daniel -d skill=lousy

generaría un fragmento POST que se vería así: name=daniel&skill=lousy

».

Envío de JSON

El uso práctico más habitual, y donde suelen producirse los errores.

La forma explícita:

curl -X POST https://api.example.com/users \
  -H "Content-Type: application/json" \
  -d '{"name":"Ada","role":"engineer"}'

El atajo que la mayoría de la gente no conoce. curl dispone de una opción específica «--json», documentada de la siguiente manera: «Envía los datos JSON especificados en una solicitud POST al servidor HTTP. --json funciona como un atajo para pasar estas tres opciones: --data-binary [arg], --header "Content-Type: application/json", --header "Accept: application/json"».

curl --json '{"name":"Ada","role":"engineer"}' https://api.example.com/users

Tres opciones en una, y establece tanto Accept como Content-Type, que suele ser lo que se desea. También lee desde un archivo o desde la entrada estándar (stdin) con @:

curl --json @payload.json https://api.example.com/users
cat payload.json | curl --json @- https://api.example.com/users

Una advertencia sincera del manual: «No se verifica que los datos pasados sean realmente JSON ni que la sintaxis sea correcta». Establece los encabezados; no los valida. Un cuerpo mal formado se sigue enviando con un tipo de contenido JSON, y la queja del servidor se referirá a tu JSON en lugar de a curl.

Los encabezados que establece «se pueden anular con --header como de costumbre», por lo que puedes mantener el atajo y ajustar una parte del mismo.

Las comillas simples son importantes. Encerra los cuerpos JSON entre comillas simples para que el shell no expanda $ ni interprete las comillas dobles que haya en su interior. Si tu JSON también contiene comillas simples, guárdalo en un archivo.

Las cinco opciones de datos y cuándo utilizar cada una

Esta es la parte que aclara la mayor parte de las dudas, ya que curl cuenta con varias variantes de «--data» que difieren en aspectos concretos.

OpciónTipo de contenido establecido¿Es especial «@»?Saltos de líneaSe utiliza para
-d / --dataform-urlencodedSí, lee un archivoEliminadosEnvíos de formularios
--data-rawform-urlencodedNoEliminadosDatos que empiezan por @
--data-binaryform-urlencodedSíConservadosArchivos, bytes exactos
--data-urlencodeform-urlencodedSíCodificadosValores con caracteres especiales
--jsonapplication/jsonSíConservadosCuerpos JSON

--data-raw existe por una razón: el manual indica que envía datos «de forma similar a --data, pero sin la interpretación especial del carácter @». Si tus datos literales comienzan por @ —una dirección de correo electrónico, un nombre de usuario, una mención—, -d intentará interpretarlos como un nombre de archivo y fallará, lo que puede resultar confuso. Su propio ejemplo es curl --data-raw "@at@at@".

--data-binary es el que hay que utilizar para los archivos. El manual: «Envía los datos exactamente tal y como se especifican, sin ningún tipo de procesamiento adicional... Se conservan los saltos de línea y los retornos de carro, y nunca se realizan conversiones». Ten en cuenta que, por defecto, sigue enviando application/x-www-form-urlencoded, por lo que, si vas a enviar datos binarios arbitrarios, el manual te indica que lo anules: -H "Content-Type: application/octet-stream".

Por eso -d @file.json puede fallar sutilmente: se eliminan los saltos de línea. En el caso del JSON, esto no suele importar; sin embargo, sí que importa en cualquier caso en el que los espacios en blanco sean significativos. --data-binary @file.json o --json @file.json son las formas más seguras.

--data-urlencode gestiona valores que contienen &, =, espacios o cualquier otro elemento que pudiera alterar la codificación de los formularios. El manual documenta varias sintaxis, y la que te interesa es casi siempre name=content, que codifica el contenido según el formato URL y deja el nombre tal cual:

curl --data-urlencode "comment=hello & goodbye = fine" https://example.com/post

Sin ello, ese «&» se interpretaría como un separador de campos y tu comentario se truncaría sin previo aviso. También existe «name@filename», que carga el contenido desde un archivo, lo codifica como URL y añade «=» al nombre.

Carga de archivos y formularios multiparte

Para la carga real de archivos, la opción es -F, y funciona de forma diferente a -d.

El manual indica: «-F, --form <name=content>... emula un formulario rellenado en el que el usuario ha pulsado el botón de envío. Esto hace que curl envíe los datos POST utilizando el tipo de contenido multipart/form-data, de acuerdo con la RFC 2388».

curl -F "file=@report.pdf" -F "title=Q3 Report" https://api.example.com/upload

Merece la pena conocer la distinción entre «@» y «<», ya que no resulta intuitiva. El manual indica: «Para forzar que la parte “content” sea un archivo, antepone al nombre del archivo el símbolo @. Para obtener la parte “content” de un archivo, antepone al nombre del archivo el símbolo <. La diferencia entre @ y < radica, pues, en que @ hace que un archivo se adjunte a la publicación como una carga de archivo, mientras que < crea un campo de texto y obtiene el contenido de dicho campo a partir de un archivo».

Así pues, @ carga un archivo como tal; < envía el contenido de un archivo como valor de un campo de texto.

Para establecer un tipo de contenido en una parte:

curl -F "file=@data.csv;type=text/csv" https://api.example.com/upload

Y si necesitas un valor literal que comience por @ o <, utiliza --form-string, que no interpreta ninguno de esos caracteres.

No establezcas tú mismo Content-Type: multipart/form-data. curl lo genera incluyendo un parámetro de delimitación, y sobrescribirlo genera una solicitud que el servidor no puede analizar —una causa muy común de errores 400 y 415 inexplicables—.

Para una solicitud PUT sencilla de un archivo, -T resulta más sencillo: «Sube el archivo local especificado a la URL remota... Si esta opción se utiliza con una URL HTTP(S), se emplea el método PUT».

Autenticación y encabezados

curl --json '{"a":1}' \
  -H "Authorization: Bearer eyJhbG..." \
  https://api.example.com/items

-H es una opción repetible y anula los valores predeterminados de curl, incluidos los que establece --json .

Para la autenticación básica, -u user:password — o simplemente -u user , lo que hace que curl solicite la contraseña para que esta no quede registrada en el historial de tu shell. El manual señala que «en los sistemas en los que funciona, curl oculta el argumento de la opción indicada de los listados de procesos», aunque añade que «esto no es suficiente para proteger las credenciales».

Para una API basada en sesiones, captura y reutiliza las cookies:

curl -c jar.txt -d "user=ada&pass=secret" https://example.com/login
curl -b jar.txt --json '{"a":1}' https://example.com/api/items

Depuración de una solicitud POST que no funciona

Una breve secuencia que resuelve casi todo.

Comprueba exactamente lo que has enviado:

curl -v --json '{"a":1}' https://api.example.com/items 2>&1 | grep -E '^[<>]'

Las líneas que empiezan por > son tu solicitud, y las que empiezan por < son la respuesta. Comprueba que el método, el Content-Type y el cuerpo sean los que pretendías. Una proporción sorprendente de los informes de «la API no funciona» se resuelven aquí.

Lee el código de estado y el cuerpo del error:

curl -sS -o body.txt -D headers.txt --json '{"a":1}' https://api.example.com/items
head -1 headers.txt; head -c 300 body.txt

Interpreta los errores más comunes:

EstadoSuele significar
400Cuerpo mal formado o falta un campo obligatorio
401Credenciales ausentes o no válidas
403Autenticado, pero sin permiso
405El punto final no acepta POST: comprueba la URL y el método
413Cuerpo demasiado grande
415«Content-Type
» incorrecto — el clásico error «-d
» con JSON
422El tipo de contenido es correcto, pero los datos no han superado la validación

La distinción entre 415 y 422 es la que vale la pena interiorizar: 415 significa que el contenedor es incorrecto, mientras que 422 significa que el contenido lo es. Lo hemos tratado en detalle en ¿Qué es un código de estado 415?.

Haz que los errores se noten en los scripts:

curl --fail-with-body --silent --show-error \
     --connect-timeout 5 --max-time 30 \
     --json @payload.json https://api.example.com/items

--fail-with-body devuelve un código de salida distinto de cero en caso de errores HTTP, al tiempo que sigue mostrando el cuerpo de la respuesta, que es lo que te interesa cuando la API devuelve mensajes de error JSON útiles. El código simple --fail descarta el cuerpo, lo que hace que se pierda la explicación.

Cómo convertir una solicitud del navegador en un comando curl

La forma más rápida de reproducir una solicitud POST que funciona en el navegador pero no en tu código —y una técnica que la mayoría de la gente descubre mucho más tarde de lo que debería.

Cópialo desde las herramientas de desarrollo. En Chrome, Firefox y Safari, abre la pestaña «Red», busca la solicitud, haz clic con el botón derecho y selecciona «Copiar como cURL». Obtendrás un comando completo con todos los encabezados, cookies y el cuerpo que envió el navegador. Pégalo en un terminal y debería comportarse de forma idéntica.

Esto responde de inmediato a la pregunta subyacente en la mayoría de las sesiones de depuración: ¿el problema está en la solicitud o en mi código? Si el comando copiado funciona y el tuyo no, la diferencia está en lo que estás enviando, y ahora tienes ambas versiones una al lado de la otra para compararlas.

A continuación, simplifícalo. Un comando copiado suele incluir una treintena de encabezados, la mayoría de los cuales son irrelevantes. Elimínalos de a pocos y vuelve a ejecutarlo hasta que falle. Lo que quede será el conjunto mínimo que el servidor realmente requiere, y eso es lo que debe incluir tu aplicación:

curl 'https://api.example.com/items' \
  -H 'content-type: application/json' \
  -H 'authorization: Bearer eyJhbG...' \
  --data-raw '{"name":"Ada"}'

Ten en cuenta que los navegadores envían --data-raw en lugar de -d, precisamente porque, de lo contrario, un cuerpo que comience por @ se interpretaría erróneamente como un nombre de archivo.

Ten en cuenta dos aspectos que no se conservarán tras la copia. Las cookies se incluyen como un encabezado literal y caducarán. Y cualquier elemento que la página haya calculado en JavaScript —un token CSRF, una firma, un valor derivado de una marca de tiempo— queda integrado en el comando copiado como una cadena fija, por lo que funciona una vez y luego deja de hacerlo. Si una solicitud reproducida tiene éxito y luego falla en la segunda ejecución, ese es casi siempre el motivo, y la solución consiste en recuperar el token en lugar de codificarlo de forma estática.

Por otro lado, existen varias herramientas que convierten un comando curl en código para la mayoría de los lenguajes, lo cual es una forma razonable de pasar de un comando que funciona a un cliente que funciona sin tener que volver a escribir los encabezados a mano.

Envío de POST a través de un proxy

Breve: lo importante es una advertencia más que una técnica.

curl -x http://user:pass@proxy.example.com:9000 \
     --json '{"a":1}' https://api.example.com/items

El funcionamiento no cambia. Lo que cambia es el cálculo de los reintentos, y esta es la parte en la que vale la pena pensar antes de añadir --retry .

Por lo general, el método POST no es idempotente. Enviarlo dos veces puede crear dos registros. La opción «--retry » de curl solo se activa en condiciones transitorias por defecto, pero «--retry-all-errors » amplía considerablemente ese ámbito; y, a través de un proxy, un error 5xx suele significar que el destino te ha rechazado, en lugar de que haya tenido un momento de fallo. Reintentarlo es, en el mejor de los casos, inútil y, en el peor, duplica una escritura.

Un tiempo de espera agotado no es prueba de fallo. Si una solicitud agota el tiempo de espera después de que el servidor la haya recibido, es posible que la operación se haya completado mientras veías un error. A través de un proxy hay un salto adicional en el que esto puede ocurrir. Si la operación es importante, utiliza una clave de idempotencia —la mayoría de las API serias la admiten— en lugar de confiar en la lógica de reintentos para estar seguro.

Y comprueba en qué salto se te ha denegado el acceso. %{http_connect} muestra la respuesta del proxy a la solicitud CONNECT por separado del estado del destino:

curl -sS -o /dev/null -x "$PROXY" \
  -w 'connect=%{http_connect} status=%{response_code}\n' \
  --json '{"a":1}' https://api.example.com/items

connect=407 significa que el proxy solicitaba credenciales. connect=200 status=403 significa que el proxy funcionó correctamente y que el destino denegó el acceso. Problemas distintos, soluciones distintas.

Preguntas frecuentes

¿Cómo envío una solicitud POST con curl?

curl -d "key=value" URL. La opción -d implica POST, por lo que -X POST es innecesario. Además, establece Content-Type: application/x-www-form-urlencoded, lo cual es correcto para el envío de formularios, pero incorrecto para JSON.

¿Cómo envío JSON mediante POST con curl?

curl --json '{"key":"value"}' URL es el atajo: establece --data-binary y, además, los encabezados Content-Type y Accept en application/json. La forma más larga es -X POST -H "Content-Type: application/json" -d '...'. Ten en cuenta que --json no valida tu JSON.

¿Por qué me aparece el error 415 «Unsupported Media Type» en curl?

Casi siempre se debe a que has utilizado -d con un cuerpo JSON sin establecer el tipo de contenido. -d envía datos codificados como formulario, y una API que espera JSON lo rechaza. Utiliza --json o añade -H "Content-Type: application/json".

¿Cuál es la diferencia entre -d y --data-raw?

-d interpreta un @ al principio como «leer desde este archivo». --data-raw no lo hace, por lo que es lo que necesitas cuando tus datos literales comienzan por @ —una dirección de correo electrónico o un identificador, por ejemplo—. Por lo demás, se comportan de forma idéntica.

¿Cómo subo un archivo con POST de curl?

curl -F "file=@document.pdf" URL, que envía multipart/form-data. Utiliza @ para adjuntar el archivo como tal y < para enviar su contenido como valor de un campo de texto. No establezcas tú mismo el encabezado Content-Type: curl lo genera con el delimitador necesario.

¿Cómo envío datos POST desde un archivo?

Utiliza curl --json @payload.json URL para JSON, o --data-binary @file para los bytes exactos, incluidos los saltos de línea. Evita -d @file cuando los espacios en blanco sean importantes, ya que -d elimina los saltos de línea y los retornos de carro.

¿Cómo envío mediante POST un valor que contiene un símbolo «&»?

Utiliza --data-urlencode "field=value with & inside". Con -d sin formato, el símbolo «&» se interpreta como un separador de campos y tu valor se trunca silenciosamente en ese punto.

¿Debo reintentar un POST que ha fallado?

Con precaución. Por lo general, POST no es idempotente, por lo que un nuevo intento podría generar un duplicado; además, un tiempo de espera agotado no garantiza que el servidor no haya procesado la solicitud. Utiliza una clave de idempotencia cuando la API la admita y ten cuidado con --retry-all-errors, especialmente a través de un proxy, donde un código 5xx suele significar un rechazo más que un fallo transitorio.

Conclusión

Todo el tema se reduce a una pregunta: ¿qué tipo de contenido espera el punto final y lo envía tu comando?

-d envía datos codificados como formulario, lo cual es adecuado para el envío de formularios pero incorrecto para JSON, y esa única discrepancia es la causa de la mayoría de los errores 415 que se producen. --json es la opción que se debe utilizar en su lugar, y es poco conocida: un indicador que configura el manejo del cuerpo y ambos encabezados, con soporte @ para archivos y stdin.

Más allá de eso, existen variantes por razones específicas que conviene recordar. --data-raw cuando tus datos empiezan por @. --data-binary cuando los saltos de línea son importantes. --data-urlencode cuando un valor contiene caracteres que romperían la codificación del formulario. -F para subidas de archivos reales, con @ para adjuntar un archivo y < para leer un campo de texto desde uno.

Y cuando algo falle, ejecútalo con -v y lee las líneas > antes de cambiar nada. La solicitud que has enviado no suele ser la que creías haber enviado, y esa diferencia es la fuente de la mayor parte de la confusión en este ámbito.