# API · Response conventions

> How amounts are typed, where a JSON payload hides inside a string and why the same entity changes shape between fetching one and listing many.

- kind: concept
- status: stable
- api_version: v1
- last_verified: 2026-10-20
- url: https://tilopay.com/en/developers/api/convenciones

The acquiring API responses have several details that you cannot infer by reading each
operation on its own, and that break typed integrations. They live here because they apply
to several operations at once.

For error handling, see [how to read an error response](/en/developers/api/procesos-operativos/errors).

## Amounts come as strings, not numbers [#montos-cadena]

> **Warning:** Amounts, discounts and totals are serialized as strings: `"discount": "40.00"`,
> `"amount": "0.00"`, `"first_amount": "0.00"`, `"amount_max_daily": "1000.00"`.

Any typed deserialization that declares those fields as numbers fails. Declare them as
strings and convert them yourself, with your language's decimal type, not with floating
point.

It shows up, among others, in [get a coupon](/en/developers/api/recurrentes/cupones/get-coupon),
[list coupons](/en/developers/api/recurrentes/cupones/get-coupons),
[list plans](/en/developers/api/recurrentes/planes/get-plans) and
[subscriber payments](/en/developers/api/recurrentes/suscriptores/suscriptor-payments).

## There is a JSON payload inside a string [#doble-encoding]

In [subscriber payments](/en/developers/api/recurrentes/suscriptores/suscriptor-payments), the
`data` field of each payment **is not an object: it is a string containing JSON**.

```json
"data": "{\"first\":1,\"expire\":\"2024-12-28T19:05:29.572648Z\",\"amount\":0,\"activeAmount\":0,\"originalAmount\":0,\"key\":\"XXXX-XXXX-XXXX-XXXX-XXXX\"}"
```

You have to deserialize twice: first the response, then the contents of `data`. Treat that
second deserialization the way you treat any external input: wrap it and log the raw string
when it does not parse.

## The same entity changes shape between "get one" and "list" [#uno-vs-lista]

This is not a documentation slip: the two operations return different shapes of the same
coupon, and each needs its own mapping.

| | [`getCoupon`](/en/developers/api/recurrentes/cupones/get-coupon) (one) | [`getRepeatCoupons`](/en/developers/api/recurrentes/cupones/get-coupons) (list) |
| --- | --- | --- |
| Creation date | `created_at`, ISO format | `create`, `Y-m-d H:i:s` format |
| Other fields | includes `recurrent_id`, `updated_at` and `deleted_at` | does not include them |

The same happens in
[subscriber payments](/en/developers/api/recurrentes/suscriptores/suscriptor-payments), which
also uses `create` with the `Y-m-d H:i:s` format.

## Two ways of saying "not configured" [#no-configurado]

In [list plans](/en/developers/api/recurrentes/planes/get-plans), a plan's webhook fields come
as an empty string when they are not configured, while `return_url` comes as `null`. Those
are two representations of the same state in the same object: when checking whether a
webhook is configured, treat the empty string and `null` as the same case.

## `status` and `type` may be numbers or strings [#status-type]

In acquiring API responses, `status` and `type` may be numbers or strings. For example, [create payments](/en/developers/api/cobros-tarjetas-guardadas/set-payments) returns `"status": 200` in the success example and `"status": "400"` in the error example; [create a group](/en/developers/api/cobros-tarjetas-guardadas/set-group) returns `"type": "200"`. Accept both types when reading these indicators; do not assume a single type across all operations.
