Skip to main content
A product (Product) is used to group and categorize your charges (debts, subscriptions, and payment links). Its use is completely optional: you can charge without associating any product. You use it when you want to report your charges by product or service line, for example “Plan Premium”, “Gym monthly fee”, or “Internet 100MB”.

Natural key: name

The name is the product’s natural key and is unique per company. If an active product with that name already exists, POST /products answers 201 with the existing product, unchanged: it does not generate a duplicate or an error.
That is why POST /products is idempotent by design: repeating the same name returns the same product, as it is stored (any other values you send in the body are ignored). The exception is externalProductId: if the existing product already has a different externalProductId, the API answers 409 RESOURCE_CONFLICT.

Create a product

The response reflects what was stored (timestamps, externalProductId, and active), not the request.
The productId is the product’s public ID, with the prd_ prefix (e.g. prd_12985). In the path of GET, PATCH, and DELETE /products/{id} the number without prefix (12985) is also accepted; other prefixes or non-numeric values return 404 RESOURCE_NOT_FOUND.
string
Your own identifier for the product (optional). It is what you later use to reference it when creating debts, without having to store Tapi’s productId. It does not accept the characters ', ", \, or ; (returns 400).

Validations

  • name and externalProductId are trimmed before being stored.
  • Fields not declared in the body are ignored.
  • If the externalProductId is already used by another product, or the name already exists with a different externalProductId, the API answers 409 RESOURCE_CONFLICT (with the same message in both cases).

Clients with several companies

If your TapiPay client has a single company, there is nothing to indicate: everything you create belongs to it. If your client manages several companies (for example, a network of schools where each school is a Tapi company), tell us which one the product belongs to with companyCode in the body:
The product is created inside that company: name and externalProductId are unique per company, so the same name can exist in each of your companies without conflict. To list the products of one company use GET /products?companyCode=MX-S-04937.
string
Tapi company (MX-S-…) the product is created in. Optional if your client has a single company. It must belong to your client.
This rule applies to the whole API, not just products: a client with several companies sends companyCode on every request (in the body for POST/PATCH, or as ?companyCode= for GET). In a PATCH, the companyCode in the body is taken as context, not as a field to modify. See API conventions. When creating debts with POST /v2/debts, the product is looked up inside the request’s company: a product from one company cannot be used in a debt of another.

Referencing a product in a debt

When creating a debt with POST /v2/debts, the product must already exist and is referenced by productId (format prd_<number>; any other format returns 400 INVALID_REQUEST) or externalProductId (only one; sending both returns 400). The debts endpoint does not create products: sending productName returns 400 INVALID_REQUEST, and referencing a nonexistent product returns 404 RESOURCE_NOT_FOUND without creating the debt.
In the debt response, the product comes embedded:
Referencing a product is what enables direct debit for that debt.

Inline creation (productName)

In subscriptions and payment links you can send the product inline with the productName field: if the name does not exist, it is created; if it already exists, it is reused without duplicating it.
This does not apply to debts. POST /v2/debts does not accept productName (it returns 400 INVALID_REQUEST) and never creates products: see Referencing a product in a debt.
In the charge response, the product comes embedded with four fields:
The createdInline field is true if the product was created in that same request, and false if it already existed and was reused. For debts it is always false.

List products

GET /products returns only active products, sorted by productId descending. Pagination uses page (default 1) and limit (default 50, maximum 500). See API conventions.

Update

PATCH /products/{id} accepts only two fields:
string
New name, non-empty string. If another product already uses it, even an inactive one, returns 409 RESOURCE_CONFLICT.
boolean
Only true or false. active: false deactivates the product just like DELETE. active: true changes nothing: a product you can read is already active, and an inactive one returns 404. It is accepted so you can send the full object.
An empty body, or any other field, returns 400 INVALID_REQUEST. Invalid types (for example "active": "no") return 400 with the detail in details[]. The companyCode in the body is taken as context, not as a field to modify.
Known limitation: updatedAt does not reflect changes made with PATCH.

Deactivate

boolean
default:"true"
Status of the product.
DELETE /products/{id} performs a soft delete: it sets active: false without deleting the product, so the history is preserved. A DELETE on an already deleted product returns 404 RESOURCE_NOT_FOUND.
An inactive product (deleted, deactivated with PATCH, or created with active: false) is out of reach of the API:
  • It does not appear in GET /products, and GET /products/{id} returns 404.
  • It cannot be reactivated through the API: a PATCH with active: true also returns 404.
  • It cannot be used in new debts, payment links, or subscriptions.
  • Its name stays taken: another product cannot be renamed to that name (409 RESOURCE_CONFLICT).
Creating a product with active: false returns the product with active: false and createdAt/updatedAt set to null.

Endpoints

Try it in the API Reference

Explore each endpoint with its interactive playground.