Caso técnicoTechnical walkthrough
«Los pagos me fallan.»
Qué hago con eso.
“My payments are failing.”
Here is what I do with that.
Un comercio abre un ticket con esa frase y nada más. No dice cuándo empezó, ni si falla todo o sólo algunos, ni qué ve exactamente. Este es el recorrido que hago (el mismo, en el mismo orden, todas las veces) hasta llegar a la causa y poder explicarla dos veces: una al comercio, sin una sola palabra técnica, y otra a Ingeniería, con el payload adjunto.
A merchant opens a ticket with that sentence and nothing else. No start time, no idea whether it's every payment or a few, no description of what they actually see. This is the path I walk (the same one, in the same order, every time) until I reach the cause and can explain it twice: once to the merchant without a single technical word, once to engineering with the payload attached.
Sobre el ejemplo. El caso es real en su estructura y el método es exactamente el que uso, pero los identificadores, los nombres y los datos están inventados, y la forma de la API está simplificada. Nada de acá expone información de ningún cliente.
About the example. The case is real in shape and the method is exactly the one I use, but every identifier, name and value is made up, and the API shape is simplified. Nothing here exposes any customer's data.
El recorrido
The walkthrough
-
Convertir la queja en un hecho verificable
Turn the complaint into a checkable fact
«Fallan los pagos» no se puede investigar. Lo primero es conseguir un caso concreto: un ID de transacción, o la hora exacta con el monto. Con eso alcanza para todo lo que viene después. Las tres preguntas que mando juntas, en un solo mensaje, para no hacer diez idas y vueltas:
“Payments are failing” is not something you can investigate. The first move is getting one concrete case: a transaction ID, or an exact timestamp plus the amount. That's enough for everything that follows. The three questions I send together, in a single message, so we don't do ten round trips:
- ¿Desde cuándo? Separa «cambió algo de su lado» de «cambió algo del nuestro».
- Since when? Separates “something changed on their side” from “something changed on ours”.
- ¿Todos o algunos? «Todos» suele ser configuración o credenciales, y «algunos» suele ser un dato puntual.
- All of them or some? “All” usually means configuration or credentials, and “some” usually means a specific piece of data.
- ¿Qué ves exactamente? Pido la respuesta cruda, no la interpretación. Un screenshot del error sirve más que un párrafo describiéndolo.
- What exactly do you see? I ask for the raw response, not the interpretation. A screenshot of the error beats a paragraph describing it.
En este caso la respuesta fue: «desde ayer a la tarde, algunos sí y otros no, nos vuelve un error 422». Con eso ya se puede empezar.
In this case the answer was: “since yesterday afternoon, some go through and some don't, we get a 422 back”. That's enough to start.
-
El status code, antes que cualquier otra cosa
The status code, before anything else
Es el primer dato porque decide de quién es el problema y evita horas de buscar en el lugar equivocado:
It comes first because it decides whose problem this is and saves hours of looking in the wrong place:
Código Code Qué significa What it means Dónde miro Where I look 400La petición está mal formada Malformed request El payload del comercio The merchant's payload 401/403No autenticado / sin permiso Not authenticated / not allowed Credenciales, entorno, scopes Credentials, environment, scopes 404El recurso no existe Resource doesn't exist El ID, o el entorno equivocado The ID, or the wrong environment 409Conflicto, casi siempre idempotencia Conflict, nearly always idempotency Reintentos con la misma clave Retries reusing the same key 422Bien formado, pero un valor no pasa validación Well formed, but a value fails validation El campo que nombra el body The field named in the body 429Demasiadas peticiones Too many requests Rate limit y su reintento Rate limiting and their retry logic 5xxEs nuestro It's ours Logs y escalar a Ingeniería Logs, and escalate to engineering 422 es el más útil de todos. Dice que la petición llegó bien, se entendió, y que un valor puntual no pasó. Eso descarta de una credenciales, conectividad, permisos y caída de la plataforma. Y el
4del principio dice que el origen está del lado del cliente, que no es lo mismo que decir que es su culpa: muchas veces el dato se lo mandó un tercero.422 is the most useful of the lot. It says the request arrived, was understood, and that one specific value didn't pass. That rules out credentials, connectivity, permissions and platform outage in one move. And the leading
4says the origin is on the client side, which is not the same as saying it's their fault: often a third party handed them the value. -
Buscar las peticiones reales, no las que me contaron
Find the actual requests, not the reported ones
Lo que el comercio cuenta y lo que su servidor mandó no siempre coinciden. Voy a la base y traigo los intentos del comercio en la ventana que dijo, con el estado y el error, para ver el patrón antes de mirar un caso suelto:
What the merchant describes and what their server actually sent don't always match. I go to the database and pull their attempts inside the window they mentioned, with status and error, to see the pattern before looking at any single case:
-- Intentos del comercio en la ventana reportada, agrupados por error -- Merchant's attempts in the reported window, grouped by error SELECT date_trunc('hour', t.created_at) AS hora, t.status, t.error_code, count(*) AS intentos, min(t.id) AS ejemplo_id FROM transactions t JOIN merchants m ON m.id = t.merchant_id WHERE m.external_id = 'mrc_8f21c0' AND t.created_at >= now() - interval '48 hours' GROUP BY 1, 2, 3 ORDER BY 1 DESC, intentos DESC;Y el resultado ya cuenta media historia:
And the result already tells half the story:
hora | status | error_code | intentos | ejemplo_id -------------------+----------+---------------------+----------+------------ 2026-09-21 18:00 | rejected | invalid_tax_id | 23 | txn_9a1f04 2026-09-21 18:00 | approved | | 41 | txn_9a1e77 2026-09-21 17:00 | approved | | 58 | 2026-09-20 17:00 | approved | | 52 |
Tres cosas quedan claras de una: empezó a las 18:00 de ayer, no falla todo (41 aprobados en la misma hora), y el error tiene nombre:
invalid_tax_id. Ya sé qué campo mirar, y tengo un caso que falló y otro que funcionó en la misma hora y del mismo comercio. Eso es oro: es el par que hace el diff limpio.Three things are already clear: it started at 18:00 yesterday, it isn't everything (41 approved in the same hour), and the error has a name:
invalid_tax_id. I know which field to look at, and I have one failing case and one succeeding case from the same hour and the same merchant. That's the pair that makes the diff clean. -
Comparar el que funciona contra el que falla
Diff the one that works against the one that breaks
Este es el paso que más veces resuelve el caso solo. Dos peticiones del mismo comercio, al mismo endpoint, con la misma versión de API: una pasa y la otra no. Todo lo que no cambia deja de ser sospechoso:
This is the step that most often solves the case by itself. Two requests from the same merchant, same endpoint, same API version: one passes and one doesn't. Everything that doesn't differ stops being a suspect:
// txn_9a1e77 · approved { "amount": 154900, "currency": "ARS", "payer": { "name": "Ana Giordano", "tax_id": "20345678901", "email": "ana@example.com" } } // txn_9a1f04 · rejected · invalid_tax_id { "amount": 236400, "currency": "ARS", "payer": { "name": "Marcos Pérez", "tax_id": "20-34567890-2", "email": "marcos@example.com" } }Ahí está. El mismo campo, el mismo tipo de dato, distinto formato: uno va sin guiones y el otro con guiones. La API espera 11 dígitos sin separadores y el segundo no pasa la validación.
There it is. Same field, same kind of data, different format: one has no separators, the other has dashes. The API expects 11 digits with no separators, and the second one fails validation.
Pero «el formato está mal» todavía no es la causa. La pregunta que importa es por qué cambió ayer a las 18:00. Se lo pregunto al comercio con esa precisión: no «revisá tu integración», sino «¿cambió algo en cómo cargan el identificador fiscal, o de dónde viene ese dato, ayer a la tarde?». La respuesta fue que habían habilitado un formulario nuevo en el checkout, que deja escribir el campo a mano.
But “the format is wrong” is not the cause yet. The question that matters is why it changed yesterday at 18:00. I ask the merchant with exactly that precision: not “check your integration”, but “did anything change yesterday afternoon in how you collect the tax ID, or where that value comes from?”. The answer: they had rolled out a new checkout form that lets the customer type the field by hand.
-
Reproducirlo antes de afirmarlo
Reproduce it before claiming it
Tengo una hipótesis, no una conclusión. Antes de escribirle al comercio la confirmo en Postman contra sandbox: mando la petición que falló tal cual, y después la misma con el campo normalizado. Si la primera da
422y la segunda201, la hipótesis pasó a ser causa.I have a hypothesis, not a conclusion. Before writing to the merchant I confirm it in Postman against sandbox: send the failing request exactly as it was, then the same one with the field normalised. If the first returns
422and the second returns201, the hypothesis just became the cause.POST /v1/payments → tax_id con guiones / with dashes 422 Unprocessable Entity { "error": "invalid_tax_id", "message": "tax_id must be 11 digits with no separators", "field": "payer.tax_id" } POST /v1/payments → mismo payload, tax_id normalizado / same payload, normalised 201 Created { "id": "txn_9b0c12", "status": "approved" }Este paso parece de más y no lo es. Es lo que separa «creo que es el formato» de «es el formato, acá está la prueba», y es lo que evita mandar a un equipo de desarrollo a cambiar algo por una corazonada mía.
This step looks optional and isn't. It's what separates “I think it's the format” from “it is the format, here's the proof”, and it's what stops me from sending someone else's engineering team to change code on a hunch of mine.
-
Explicarlo dos veces, en dos idiomas distintos
Explain it twice, in two different languages
La misma causa se escribe distinto según quién la lee. Al comercio, sin una sola palabra técnica, con la acción concreta adelante:
The same cause gets written differently depending on who reads it. To the merchant, without a single technical word, with the concrete action up front:
Encontramos el problema. Desde ayer 18:00 se rechazan los pagos en los que el identificador fiscal llega escrito con guiones. Los 23 rechazados de ayer tienen todos esa característica, y los 41 que sí pasaron llegaron sin guiones.
El formulario nuevo del checkout deja escribirlo a mano, y ahí entran los guiones. Si le sacan los separadores antes de mandárnoslo, se resuelve. Ninguno de esos cobros se perdió: se pueden reintentar tal cual.
We found it. Since 18:00 yesterday, payments are being declined when the tax ID reaches us written with dashes. All 23 declines from yesterday have that in common, and the 41 that went through arrived without them.
The new checkout form lets customers type it by hand, and that's where the dashes come from. Stripping the separators before sending solves it. None of those charges are lost: they can be retried as they are.
A Ingeniería, lo mismo pero accionable: los dos IDs, el
error_code, el payload de cada uno, la hora de corte y la consulta que lo muestra. Un ticket que se puede tomar sin volver a preguntarme nada.To engineering, the same thing but actionable: both IDs, the
error_code, each payload, the cutoff timestamp and the query that shows it. A ticket someone can pick up without coming back to ask me anything. -
Cerrar con la confirmación, no con el diagnóstico
Close on confirmation, not on diagnosis
El ticket no se cierra cuando explico la causa: se cierra cuando veo las transacciones aprobadas. Corro la misma consulta del paso 3 al día siguiente, confirmo que
invalid_tax_iddesapareció, y recién ahí cierro, con el número adentro del mensaje, para que el comercio no tenga que verificarlo él.The ticket doesn't close when I explain the cause: it closes when I see the approved transactions. I run the same query from step 3 the next day, confirm
invalid_tax_idis gone, and only then close it, with the number inside the message, so the merchant doesn't have to go check it themselves. -
La pregunta que hace que el ticket no vuelva
The question that stops the ticket coming back
Acá termina el soporte y empieza la parte que me interesa: ¿es la primera vez que veo esto? Busco el mismo
error_codeen todos los comercios, no sólo en este:This is where support ends and the part I care about begins: is this the first time I see this? I look for the same
error_codeacross every merchant, not just this one:SELECT m.external_id, count(*) AS rechazos, min(t.created_at) AS primero, max(t.created_at) AS ultimo FROM transactions t JOIN merchants m ON m.id = t.merchant_id WHERE t.error_code = 'invalid_tax_id' AND t.created_at >= now() - interval '90 days' GROUP BY 1 HAVING count(*) > 5 ORDER BY rechazos DESC;Si vuelve un solo comercio, fue un caso. Si vuelven nueve, no es un problema de soporte: es un problema de producto, y la respuesta correcta deja de ser contestar mejor. Según qué muestre la consulta, lo que sigue es una de estas tres, en este orden de preferencia:
If one merchant comes back, it was a case. If nine do, it isn't a support problem, it's a product problem, and the right answer stops being “reply faster”. Depending on what the query shows, the next move is one of these three, in this order of preference:
- Producto: que la API normalice el campo, o que el mensaje de error diga el formato esperado en vez de sólo
invalid_tax_id. Es el arreglo que hace que el ticket deje de existir para todos. - Product: have the API normalise the field, or have the error message state the expected format instead of just
invalid_tax_id. This is the fix that makes the ticket stop existing for everyone. - Documentación: si la API no va a cambiar, que el formato esperado esté en el doc con un ejemplo válido y uno inválido al lado.
- Documentation: if the API isn't going to change, put the expected format in the docs with a valid and an invalid example side by side.
- Detección: una alerta que avise cuando un
error_codese dispara en un comercio, para llegar antes que el ticket. De acá salió, de hecho, la rutina automatizada que corre todos los días. - Detection: an alert that fires when an
error_codespikes for a merchant, so we get there before the ticket does. This is in fact where the automated routine that runs every day came from.
- Producto: que la API normalice el campo, o que el mensaje de error diga el formato esperado en vez de sólo
Por qué siempre en este orden
Why always in this order
Porque cada paso descarta un conjunto entero de causas y el siguiente sale más barato. El status code descarta credenciales, red y caída de plataforma en cinco segundos. La consulta descarta «falla todo» y acota la ventana. El diff descarta todo lo que las dos peticiones tienen en común. Postman descarta que yo esté equivocado.
Because every step eliminates a whole class of causes and makes the next one cheaper. The status code rules out credentials, network and platform outage in five seconds. The query rules out “everything is broken” and narrows the window. The diff rules out everything the two requests have in common. Postman rules out me being wrong.
El error que más tiempo cuesta es saltar al final: leer «pagos fallando», suponer una causa y empezar a probar arreglos. Cuando la suposición es correcta se gana media hora, y cuando no lo es, se pierden dos días y la confianza del comercio.
The most expensive mistake is jumping to the end: reading “payments failing”, assuming a cause and testing fixes. When the guess is right you save half an hour, and when it isn't, you lose two days and the merchant's trust.
Lo que haría distinto. En este caso pregunté «¿desde cuándo?» y esperé la respuesta antes de ir a la base. Estuvo de más: la consulta del paso 3, con una ventana de 48 horas, me daba la hora de corte sola y sin depender de la memoria de nadie. Ahora abro la base primero y uso las preguntas para confirmar, no para orientarme.
What I'd do differently. Here I asked “since when?” and waited for the answer before going to the database. That was wasted time: the step-3 query with a 48-hour window gave me the cutoff on its own, without depending on anyone's memory. Now I open the database first and use the questions to confirm, not to orient myself.
¿Querés conversar sobre esto? Escribime.
Want to talk about this? Drop me a line.
sebastianlategano@gmail.com Ver el CVSee the CV