# Constancias de retenciones

> Timbre la constancia de retenciones e información de pagos (CFDI de Retenciones 2.0) de lo que retuvo a un proveedor: honorarios, arrendamiento, dividendos, intereses o pagos al extranjero.

Cuando una empresa le paga a una persona física por honorarios o arrendamiento, le retiene ISR e IVA y debe darle una **constancia de retenciones**: un CFDI de Retenciones 2.0, distinto de una factura, con lo que pagó y lo que retuvo en el periodo. Con la API la timbra con los mismos folios (RET1, RET2…), revisiones y timbres que **Retenciones** en la app. Cada constancia usa un timbre; cancelarla no cuesta.

## Timbrar una

Lo que le retuvo en septiembre a un abogado por $10,000 de honorarios: 10% de ISR y dos terceras partes del IVA.

```bash tab="cURL"
curl https://api.fiscalisto.com/v1/retentions \
  -H "Authorization: Bearer $FISCALISTO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: ret-2026-09-gora" \
  -d '{
    "code": "01",
    "recipient": {
      "tax_id": "GORA800101AB1",
      "legal_name": "ANA GÓMEZ RUIZ",
      "zip": "06000"
    },
    "period": { "year": 2026, "month": 9 },
    "totals": { "operation": 10000 },
    "taxes": [
      { "tax": "isr", "base": 10000, "amount": 1000 },
      { "tax": "iva", "base": 10000, "amount": 1066.67 }
    ],
    "send_email": true,
    "email": "ana@example.com"
  }'
```

```ts tab="TypeScript"
import { Fiscalisto } from "fiscalisto";

// La llave viene de FISCALISTO_API_KEY.
const fiscalisto = new Fiscalisto();

const retention = await fiscalisto.retentions.create({
  code: "01",
  recipient: {
    tax_id: "GORA800101AB1",
    legal_name: "ANA GÓMEZ RUIZ",
    zip: "06000",
  },
  period: { year: 2026, month: 9 },
  totals: { operation: 10000 },
  taxes: [
    { tax: "isr", base: 10000, amount: 1000 },
    { tax: "iva", base: 10000, amount: 1066.67 },
  ],
  send_email: true,
  email: "ana@example.com",
}, { idempotencyKey: "ret-2026-09-gora" });
```

```python tab="Python"
from fiscalisto import Fiscalisto

# La llave viene de FISCALISTO_API_KEY.
fiscalisto = Fiscalisto()

retention = fiscalisto.retentions.create({
    "code": "01",
    "recipient": {
        "tax_id": "GORA800101AB1",
        "legal_name": "ANA GÓMEZ RUIZ",
        "zip": "06000",
    },
    "period": {"year": 2026, "month": 9},
    "totals": {"operation": 10000},
    "taxes": [
        {
            "tax": "isr",
            "base": 10000,
            "amount": 1000,
        },
        {
            "tax": "iva",
            "base": 10000,
            "amount": 1066.67,
        },
    ],
    "send_email": True,
    "email": "ana@example.com",
}, idempotency_key="ret-2026-09-gora")
```

```php tab="PHP"
<?php

use Fiscalisto\Fiscalisto;

// La llave viene de FISCALISTO_API_KEY.
$fiscalisto = new Fiscalisto();

$retention = $fiscalisto->retentions->create([
    'code' => '01',
    'recipient' => [
        'tax_id' => 'GORA800101AB1',
        'legal_name' => 'ANA GÓMEZ RUIZ',
        'zip' => '06000',
    ],
    'period' => ['year' => 2026, 'month' => 9],
    'totals' => ['operation' => 10000],
    'taxes' => [
        [
            'tax' => 'isr',
            'base' => 10000,
            'amount' => 1000,
        ],
        [
            'tax' => 'iva',
            'base' => 10000,
            'amount' => 1066.67,
        ],
    ],
    'send_email' => true,
    'email' => 'ana@example.com',
], ['idempotency_key' => 'ret-2026-09-gora']);
```

```go tab="Go"
package main

import (
	"context"
	"fmt"
	"log"

	fiscalisto "fiscalisto.com/go"
)

func main() {
	// La llave viene de FISCALISTO_API_KEY.
	client, err := fiscalisto.NewClient("")
	if err != nil {
		log.Fatal(err)
	}
	ctx := context.Background()

	retention, err := client.Retentions.Create(ctx, &fiscalisto.CreateRetentionParams{
		Code: "01",
		Recipient: fiscalisto.CreateRetentionParamsRecipient{
			TaxID:     fiscalisto.String("GORA800101AB1"),
			LegalName: "ANA GÓMEZ RUIZ",
			Zip:       fiscalisto.String("06000"),
		},
		Period: fiscalisto.CreateRetentionParamsPeriod{
			Year:  2026,
			Month: fiscalisto.Int64(9),
		},
		Totals: fiscalisto.CreateRetentionParamsTotals{Operation: 10000},
		Taxes: []fiscalisto.CreateRetentionParamsTax{
			{Tax: "isr", Base: 10000, Amount: 1000},
			{Tax: "iva", Base: 10000, Amount: 1066.67},
		},
		SendEmail: fiscalisto.Bool(true),
		Email:     fiscalisto.String("ana@example.com"),
	}, fiscalisto.WithIdempotencyKey("ret-2026-09-gora"))
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(retention.ID)
}
```

```java tab="Java"
import com.fiscalisto.FiscalistoClient;
import com.fiscalisto.RequestOptions;
import com.fiscalisto.model.CreateRetentionParams;
import com.fiscalisto.model.CreateRetentionParamsPeriod;
import com.fiscalisto.model.CreateRetentionParamsRecipient;
import com.fiscalisto.model.CreateRetentionParamsTax;
import com.fiscalisto.model.CreateRetentionParamsTotals;
import java.util.List;

// La llave viene de FISCALISTO_API_KEY.
var fiscalisto = new FiscalistoClient();

var retention = fiscalisto.retentions().create(new CreateRetentionParams()
        .setCode("01")
        .setRecipient(new CreateRetentionParamsRecipient()
                .setTaxId("GORA800101AB1")
                .setLegalName("ANA GÓMEZ RUIZ")
                .setZip("06000"))
        .setPeriod(new CreateRetentionParamsPeriod().setYear(2026).setMonth(9))
        .setTotals(new CreateRetentionParamsTotals().setOperation(10000))
        .setTaxes(List.of(
                new CreateRetentionParamsTax()
                        .setTax("isr")
                        .setBase(10000)
                        .setAmount(1000),
                new CreateRetentionParamsTax()
                        .setTax("iva")
                        .setBase(10000)
                        .setAmount(1066.67)
        ))
        .setSendEmail(true)
        .setEmail("ana@example.com"), RequestOptions.idempotencyKey("ret-2026-09-gora"));
System.out.println(retention.getId());
```

```csharp tab="C#"
using Fiscalisto;

// La llave viene de FISCALISTO_API_KEY.
var fiscalisto = new FiscalistoClient();

var retention = await fiscalisto.Retentions.CreateAsync(new CreateRetentionParams
{
    Code = "01",
    Recipient = new()
    {
        TaxId = "GORA800101AB1",
        LegalName = "ANA GÓMEZ RUIZ",
        Zip = "06000",
    },
    Period = new() { Year = 2026, Month = 9 },
    Totals = new() { Operation = 10000 },
    Taxes = new()
    {
        new() { Tax = "isr", Base = 10000, Amount = 1000 },
        new()
        {
            Tax = "iva",
            Base = 10000,
            Amount = 1066.67m,
        },
    },
    SendEmail = true,
    Email = "ana@example.com",
}, new RequestOptions { IdempotencyKey = "ret-2026-09-gora" });
```

La respuesta es la constancia (`ret_…`) con `status: "valid"`, su `uuid`, su folio en `internal_folio` y el total retenido en `totals.withheld`. Llega también el webhook `retention.stamped`. Con `send_email` el receptor recibe el PDF y el XML en `email`.

| Campo | Qué es |
| --- | --- |
| `code` | La clave de la retención (c_CveRetenc): `01` servicios profesionales, `05` arrendamiento, `14` dividendos, `16` intereses, `18` pagos a residentes en el extranjero, `25` otro tipo (con `description`), y las demás del catálogo. Las que llevan complemento están abajo. |
| `recipient` | A quién le retuvo: su RFC, su nombre y su código postal como en su constancia de situación fiscal (el SAT los compara). Un residente en el extranjero va con `foreign: true`, su número de identificación fiscal en `tax_id` y sin código postal. |
| `period` | El mes (`month`) o los meses (`from_month` y `to_month`) del año que cubre, ya terminados o el que corre. Una constancia anual de un proveedor va con `from_month: 1` y `to_month: 12`. |
| `totals` | El monto de la operación; `taxed` y `exempt` si una parte está exenta (sin ellos, todo es gravado). |
| `taxes` | Cada impuesto retenido: `isr`, `iva` o `ieps`, su base y el monto. `payment_type` es el tipo de pago del SAT (c_TipoPagoRet); por omisión `04` para ISR (pago provisional), `01` para IVA y `02` para IEPS. |

Las claves que el SAT pide con complemento llevan su objeto, con los nombres de la referencia:

| `code` | Objeto | Complemento del SAT |
| --- | --- | --- |
| `14` | `dividends` | Dividendos 1.0 |
| `16` | `interest` | Intereses 1.0 |
| `17` | `trust_lease` | Arrendamiento en fideicomiso 1.0 |
| `18` | `foreign_payment` | Pagos a extranjeros 1.0 |
| `19` | `share_sale` | Enajenación de acciones 1.0 |
| `20` | `prize` | Premios 1.0 (`state` y los montos de `totals`) |
| `21` | `trust` | Fideicomiso no empresarial 1.0 |
| `22` | `retirement_plan` | Planes de retiro 1.1 |
| `23` | `mortgage_interest` | Intereses hipotecarios 1.0 |
| `24` | `financial_derivatives` | Operaciones con derivados 1.0 |

Cada objeto va solo con su clave; con otra contesta `400`. Sin él, la constancia contesta `422` `retention_invalid` y dice qué falta. La clave `26` (servicios mediante plataformas tecnológicas) aún no se puede emitir: contesta `422`.

```json
{
  "code": "20",
  "recipient": { "tax_id": "PEPJ800101AB1", "legal_name": "JUAN PEREZ", "zip": "06700" },
  "period": { "year": 2026, "month": 9 },
  "totals": { "operation": 50000 },
  "taxes": [{ "tax": "isr", "base": 50000, "amount": 500, "payment_type": "03" }],
  "prize": { "state": "09" }
}
```

La misma `Idempotency-Key` otra vez contesta la misma constancia, sin timbrar otra. Si el SAT la rechaza, contesta `422` `retention_rejected` con su código en `sat_code`, y llega `retention.failed`.

## Mandarla por correo después

```bash tab="cURL"
curl https://api.fiscalisto.com/v1/retentions/ret_PhBIUaMpw23r7krC5Vs3pDT6dB7C2wuY0D/send \
  -H "Authorization: Bearer $FISCALISTO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "to": "ana@example.com, contabilidad@example.com" }'
```

```ts tab="TypeScript"
import { Fiscalisto } from "fiscalisto";

// La llave viene de FISCALISTO_API_KEY.
const fiscalisto = new Fiscalisto();

const retention = await fiscalisto.retentions.send("ret_PhBIUaMpw23r7krC5Vs3pDT6dB7C2wuY0D", {
  to: "ana@example.com, contabilidad@example.com",
});
```

```python tab="Python"
from fiscalisto import Fiscalisto

# La llave viene de FISCALISTO_API_KEY.
fiscalisto = Fiscalisto()

retention = fiscalisto.retentions.send("ret_PhBIUaMpw23r7krC5Vs3pDT6dB7C2wuY0D", {
    "to": "ana@example.com, contabilidad@example.com",
})
```

```php tab="PHP"
<?php

use Fiscalisto\Fiscalisto;

// La llave viene de FISCALISTO_API_KEY.
$fiscalisto = new Fiscalisto();

$retention = $fiscalisto->retentions->send('ret_PhBIUaMpw23r7krC5Vs3pDT6dB7C2wuY0D', [
    'to' => 'ana@example.com, contabilidad@example.com',
]);
```

```go tab="Go"
package main

import (
	"context"
	"fmt"
	"log"

	fiscalisto "fiscalisto.com/go"
)

func main() {
	// La llave viene de FISCALISTO_API_KEY.
	client, err := fiscalisto.NewClient("")
	if err != nil {
		log.Fatal(err)
	}
	ctx := context.Background()

	retention, err := client.Retentions.Send(ctx, "ret_PhBIUaMpw23r7krC5Vs3pDT6dB7C2wuY0D", &fiscalisto.SendRetentionParams{
		To: fiscalisto.String("ana@example.com, contabilidad@example.com"),
	})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(retention.ID)
}
```

```java tab="Java"
import com.fiscalisto.FiscalistoClient;
import com.fiscalisto.model.SendRetentionParams;

// La llave viene de FISCALISTO_API_KEY.
var fiscalisto = new FiscalistoClient();

var retention = fiscalisto.retentions().send("ret_PhBIUaMpw23r7krC5Vs3pDT6dB7C2wuY0D", new SendRetentionParams()
        .setTo("ana@example.com, contabilidad@example.com"));
System.out.println(retention.getId());
```

```csharp tab="C#"
using Fiscalisto;

// La llave viene de FISCALISTO_API_KEY.
var fiscalisto = new FiscalistoClient();

var retention = await fiscalisto.Retentions.SendAsync("ret_PhBIUaMpw23r7krC5Vs3pDT6dB7C2wuY0D", new SendRetentionParams
{
    To = "ana@example.com, contabilidad@example.com",
});
```

Sin `to`, va al correo que tiene guardado el receptor. Si no se pudo mandar, contesta `email_failed` y dice por qué; dos envíos a la vez de la misma constancia también (el segundo no se manda).

## Descargarla

[`GET /v1/retentions/{id}/pdf`](https://fiscalisto.com/docs/api/referencia/retenciones/get-retention-pdf) da el PDF y [`/xml`](https://fiscalisto.com/docs/api/referencia/retenciones/get-retention-xml) el XML timbrado. [`GET /v1/retentions`](https://fiscalisto.com/docs/api/referencia/retenciones/list-retentions) las lista, las más recientes primero, y filtra por `status` (`valid`, `canceled`, `rejected`, `processing`), por `recipient` (un RFC) y por `year`.

## Cancelarla

```bash tab="cURL"
curl https://api.fiscalisto.com/v1/retentions/ret_PhBIUaMpw23r7krC5Vs3pDT6dB7C2wuY0D/cancel \
  -H "Authorization: Bearer $FISCALISTO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "motive": "02" }'
```

```ts tab="TypeScript"
import { Fiscalisto } from "fiscalisto";

// La llave viene de FISCALISTO_API_KEY.
const fiscalisto = new Fiscalisto();

const retention = await fiscalisto.retentions.cancel("ret_PhBIUaMpw23r7krC5Vs3pDT6dB7C2wuY0D", { motive: "02" });
```

```python tab="Python"
from fiscalisto import Fiscalisto

# La llave viene de FISCALISTO_API_KEY.
fiscalisto = Fiscalisto()

retention = fiscalisto.retentions.cancel("ret_PhBIUaMpw23r7krC5Vs3pDT6dB7C2wuY0D", {"motive": "02"})
```

```php tab="PHP"
<?php

use Fiscalisto\Fiscalisto;

// La llave viene de FISCALISTO_API_KEY.
$fiscalisto = new Fiscalisto();

$retention = $fiscalisto->retentions->cancel('ret_PhBIUaMpw23r7krC5Vs3pDT6dB7C2wuY0D', ['motive' => '02']);
```

```go tab="Go"
package main

import (
	"context"
	"fmt"
	"log"

	fiscalisto "fiscalisto.com/go"
)

func main() {
	// La llave viene de FISCALISTO_API_KEY.
	client, err := fiscalisto.NewClient("")
	if err != nil {
		log.Fatal(err)
	}
	ctx := context.Background()

	retention, err := client.Retentions.Cancel(ctx, "ret_PhBIUaMpw23r7krC5Vs3pDT6dB7C2wuY0D", &fiscalisto.CancelRetentionParams{
		Motive: fiscalisto.String("02"),
	})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(retention.ID)
}
```

```java tab="Java"
import com.fiscalisto.FiscalistoClient;
import com.fiscalisto.model.CancelRetentionParams;

// La llave viene de FISCALISTO_API_KEY.
var fiscalisto = new FiscalistoClient();

var retention = fiscalisto.retentions().cancel("ret_PhBIUaMpw23r7krC5Vs3pDT6dB7C2wuY0D", new CancelRetentionParams().setMotive("02"));
System.out.println(retention.getId());
```

```csharp tab="C#"
using Fiscalisto;

// La llave viene de FISCALISTO_API_KEY.
var fiscalisto = new FiscalistoClient();

var retention = await fiscalisto.Retentions.CancelAsync("ret_PhBIUaMpw23r7krC5Vs3pDT6dB7C2wuY0D", new CancelRetentionParams { Motive = "02" });
```

Una constancia se cancela de inmediato: el receptor no tiene que aceptarlo. Llega `retention.canceled`. Con el motivo `01` (tenía errores y otra la sustituye), timbre antes la correcta y mándela en `substitution` (su `ret_…` o su UUID): debe ser otra constancia vigente del mismo emisor.
