Postman + app real: cómo resolver un 400 en Customer List usando CSRF dinámico
Reproducir un flujo real en Postman no es solo copiar requests. Cómo resolver un 400 Bad Request causado por un CSRF token estático, usando variables dinámicas y pre-request scripts.
Después de leer la página de login en DevTools (https://demo.serenity.is/Account/Login/?ReturnUrl=%2F) y analizar el POST de autenticación, el siguiente paso lógico era reproducir ese flujo en Postman.
No fue tan directo como parecía.
La colección armada
Para este punto, la colección en Postman ya tenía tres requests:
GET - Login PagePOST - LoginPOST - Customer List
La idea era que corrieran en serie: la primera trae la página de login y deja cookies iniciales, la segunda envía usuario y contraseña, la tercera pide los datos de la grilla de clientes.
Las requests se importaron desde Chrome DevTools usando Copy as cURL (bash) y después se simplificaron manualmente dentro de Postman.
Ese detalle importa: importar desde DevTools sirve para traer una versión muy fiel de lo que hace el navegador, pero después hay que entender qué parte es realmente necesaria y cuál es ruido.

El síntoma: la tercera request fallaba
Al correr la colección, el flujo no era estable:
| Request | Resultado |
|---|---|
| GET - Login Page | 200 |
| POST - Login | 200 (a veces) |
| POST - Customer List | 400 Bad Request |

Y algo todavía más raro: si se volvía a ejecutar GET - Login Page, el login podía volver a fallar, a menos que se actualizara manualmente cierto valor.

Eso apuntaba a que el problema no estaba en la URL ni en el body principal, sino en algún dato de seguridad que la app esperaba fresco en cada ciclo.
La pista real: el token CSRF cambiaba
En los pasos anteriores:

ya se había visto que la app trabaja con cookies como CSRF-TOKEN, .AspNetCore.Antiforgery y AspNetAuth. Y que en el login real del navegador aparecía un header x-csrf-token.

Lo clave fue notar esto: cada vez que se vuelve a pedir la página de login, el valor de CSRF-TOKEN cambia.
El error de fondo era tratar el token como si fuera un valor copiable y semi-fijo. No lo es. Es un valor dinámico, generado por la app en cada ciclo.
Entonces el problema no era "cómo copiar la request". El problema era: cómo hacer que Postman deje de depender de un token viejo y use siempre el token actual.
Por qué la request "correcta" fallaba
La request tenía todo aparentemente bien: URL correcta, método correcto, body correcto, cookies presentes. Pero eso no alcanzaba.
La app esperaba que el request llevara un valor de CSRF coherente con la cookie actual. Si el valor del header quedaba viejo o no se actualizaba con cada ejecución, la request se rompía.
La solución: convertir el CSRF en una variable dinámica
En vez de dejar un valor hardcodeado, el token pasa a una variable que Postman refresca automáticamente.
El flujo queda así:
GET - Login Pageactualiza la cookie jar de PostmanPOST - Loginlee el valor actual de CSRF-TOKEN desde esa cookie jar- Ese valor se guarda en una variable
- El header
x-csrf-tokenusa esa variable POST - Customer Listtambién refresca el mismo token antes de ejecutarse
Ahí deja de ser una request "estática" y pasa a ser un flujo real.
Implementación en POST - Login
Header
x-csrf-token: {{csrfToken}}
En vez de pegar un token fijo, se usa la variable csrfToken.

Script en Pre-request
const csrfCookie = pm.cookies.get('CSRF-TOKEN');
if (csrfCookie) {
pm.variables.set('csrfToken', csrfCookie);
console.log('csrfToken actualizado desde cookie jar');
} else {
console.log('No se encontró la cookie CSRF-TOKEN');
}

La lógica es simple: Postman busca la cookie CSRF-TOKEN, si la encuentra guarda su valor en la variable csrfToken, y después el header usa ese valor actualizado.
Con eso, POST - Login ya no depende de copiar el token a mano.
Pero todavía faltaba una pieza
Con el login resuelto, POST - Customer List seguía fallando.
Esa request también necesita trabajar con un token actualizado. No alcanza con que el login lo haga bien. La request de la grilla también debe resolver el valor antes de ejecutarse.
Header
x-csrf-token: {{csrfToken}}
Script en Pre-request
const csrfCookie = pm.cookies.get('CSRF-TOKEN');
if (csrfCookie) {
pm.variables.set('csrfToken', csrfCookie);
console.log('csrfToken actualizado en Customer List desde cookie jar');
} else {
console.log('No se encontró CSRF-TOKEN en Customer List');
}

Ese fue el cambio que cerró el circuito. Una vez que Customer List levanta el token actual desde la cookie jar, deja de quedarse pegado a un valor viejo.
El resultado
Después de esos cambios, al correr la colección:
| Request | Resultado |
|---|---|
| GET - Login Page | 200 |
| POST - Login | 200 |
| POST - Customer List | 200 |

Recién ahí el flujo se vuelve real y reutilizable. No es solo "mandar requests". Es manejar cookies, autenticación, tokens dinámicos y dependencia entre requests.
La lección central
La lección más importante de este paso no es técnica. Es conceptual.
Es tentador pensar: "si la request se parece mucho a la del navegador, debería andar". A veces alcanza. A veces no.
En una app con seguridad real, ciertos valores no se "copian". Se resuelven en tiempo de ejecución.
El CSRF token no es un dato más. Es parte del contrato de seguridad de la aplicación. Por eso no sirve pegarlo una vez, no sirve confiar en un valor viejo, y no sirve asumir que la request va a seguir funcionando horas después.
Hay que leerlo dinámicamente.
Las capas de estado en Postman
Postman maneja varias capas de estado simultáneamente, y al principio eso puede marear:
- Una request puede tener headers visibles en su pestaña Headers
- El botón Cookies muestra lo que hay en la cookie jar
- Algunas cookies se agregan automáticamente cuando una response trae Set-Cookie
- Una variable como
csrfTokenpuede existir sin crearla manualmente, porque un script la setea al vuelo - Una request puede funcionar sola pero fallar dentro de un flujo, o al revés
Cuando se entiende que Postman está manejando tres cosas al mismo tiempo — el request visible, las cookies persistidas por dominio, y las variables que scripts van actualizando — deja de parecer aleatorio.
Por qué importar desde DevTools sirve, pero no alcanza
Importar con Copy as cURL (bash) es útil porque trae a Postman una versión muy parecida a la request real del navegador. Sirve para no inventar la request, partir de algo fiel a la app, y ver método, URL, headers, body y cookies iniciales.

Pero no alcanza por sí solo. Porque una vez importado, todavía hay que separar: qué headers son realmente necesarios, qué valores son dinámicos, qué cosas puede manejar Postman por su cuenta, y qué parte conviene automatizar con scripts.
Copy as cURL trae la foto del momento. Los scripts y variables dan comportamiento reutilizable.
Resumen
Ahora la colección puede: abrir la página de login, capturar contexto de seguridad, loguearse, y pedir la grilla de clientes — todo con tokens dinámicos.
La diferencia parece pequeña, pero no lo es.
Antes eran tres requests copiadas.
Ahora es un flujo que entiende estado.
Pero lo más valioso no es la colección en sí. Es entender por qué antes fallaba. Una request que "anda" sin saber por qué sirve poco. Una request que falló, se desarmó, se entendió y se hizo estable — eso es API testing real.
Código final
POST - Login → Pre-request
const csrfCookie = pm.cookies.get('CSRF-TOKEN');
if (csrfCookie) {
pm.variables.set('csrfToken', csrfCookie);
console.log('csrfToken actualizado desde cookie jar');
} else {
console.log('No se encontró la cookie CSRF-TOKEN');
}
POST - Login → Header
x-csrf-token: {{csrfToken}}
POST - Customer List → Pre-request
const csrfCookie = pm.cookies.get('CSRF-TOKEN');
if (csrfCookie) {
pm.variables.set('csrfToken', csrfCookie);
console.log('csrfToken actualizado en Customer List desde cookie jar');
} else {
console.log('No se encontró CSRF-TOKEN en Customer List');
}
POST - Customer List → Header
x-csrf-token: {{csrfToken}}
Qué sigue
El siguiente paso es convertir estas requests en verificaciones reales — no solo requests que responden 200, sino tests que validen estructura, datos y comportamiento del backend.
Esta serie se construye desde una app real (https://demo.serenity.is/Account/Login/?ReturnUrl=%2F), no desde una API pública de práctica aislada: leyendo requests en DevTools, llevándolas a Postman y entendiendo por qué fallan antes de automatizarlas.
