From fe41df5682ef706f289ebe398ac30723f4ca63f9 Mon Sep 17 00:00:00 2001 From: "raul@facturapi.io" Date: Wed, 1 Jul 2026 16:33:21 -0600 Subject: [PATCH] chore(retentions): add new retentions method for support drafts --- website/openapi_v2.en.yaml | 291 ++++++++++++++++++++++++++++++++++++- website/openapi_v2.yaml | 290 +++++++++++++++++++++++++++++++++++- 2 files changed, 565 insertions(+), 16 deletions(-) diff --git a/website/openapi_v2.en.yaml b/website/openapi_v2.en.yaml index cda57928..dbc285cb 100644 --- a/website/openapi_v2.en.yaml +++ b/website/openapi_v2.en.yaml @@ -5821,6 +5821,11 @@ paths: summary: Create retention description: | Create a new Retention. If the invoice is created in Live environment, it will be **stamped and sent to SAT**. + + To create a draft retention, send `status: "draft"`. In that case, the + retention will be saved without stamping, will not be sent to the PAC, and + may be incomplete. Facturapi sets `is_ready_to_stamp: true` only when the + draft has all required data to be stamped. x-codeSamples: - lang: Bash label: cURL @@ -5955,7 +5960,13 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/Retention" + type: object + discriminator: + propertyName: status + mapping: + valid: "#/components/schemas/Retention" + pending: "#/components/schemas/Retention" + draft: "#/components/schemas/Retention" "400": $ref: "#/components/responses/BadRequest" "401": @@ -6098,6 +6109,25 @@ paths: schema: type: string description: ID of the customer to filter by. Only retentions issued to this customer will be returned. + - in: query + name: status + schema: + oneOf: + - type: string + enum: + - draft + - pending + - valid + - canceled + - type: array + items: + type: string + enum: + - draft + - pending + - valid + - canceled + description: Filter by one or more retention statuses. - $ref: "#/components/parameters/SearchDate" - $ref: "#/components/parameters/SearchPage" - $ref: "#/components/parameters/SearchLimit" @@ -6187,6 +6217,94 @@ paths: $ref: "#/components/responses/RateLimited" "500": $ref: "#/components/responses/UnexpectedError" + put: + operationId: updateDraftRetention + tags: + - retention + summary: Edit draft retention + description: | + Updates the information of a draft retention, setting only the values for + the parameters sent in the request. Parameters not sent in the request + will not be modified. + + Facturapi recalculates `is_ready_to_stamp` after every edit. If the + retention is no longer in `draft` status, the call returns an error. + x-codeSamples: + - lang: Bash + label: cURL + source: | + curl https://www.facturapi.io/v2/retentions/6062d9fb226600001cd22f71 \ + -X PUT \ + -H "Authorization: Bearer sk_test_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "folio_int": "R-2026-001" + }' + - lang: JavaScript + label: Node.js + source: | + import Facturapi from 'facturapi' + const facturapi = new Facturapi('sk_test_API_KEY'); + const retention = await facturapi.retentions.updateDraft( + '6062d9fb226600001cd22f71', + { folio_int: 'R-2026-001' } + ); + - lang: csharp + label: C# + source: | + var facturapi = new FacturapiClient("sk_test_API_KEY"); + var retention = await facturapi.Retention.UpdateDraftAsync( + "6062d9fb226600001cd22f71", + new Dictionary + { + ["folio_int"] = "R-2026-001" + } + ); + - lang: Java + label: Java + source: | + import io.facturapi.Facturapi; + import java.util.Map; + + Facturapi facturapi = new Facturapi("sk_test_API_KEY"); + + var retention = facturapi.retentions().updateDraft( + "ret_123", + Map.of("folio_int", "R-2026-001") + ); + - lang: PHP + source: | + $facturapi = new Facturapi("sk_test_API_KEY"); + $retention = $facturapi->Retentions->updateDraft("6062d9fb226600001cd22f71", [ + "folio_int" => "R-2026-001" + ]); + parameters: + - in: path + name: retention_id + schema: + type: string + required: true + description: ID of the retention to edit + requestBody: + $ref: "#/components/requestBodies/RetentionCreate" + security: + - "SecretLiveKey": [] + - "SecretTestKey": [] + responses: + "200": + description: "`Retention` object updated successfully" + content: + application/json: + schema: + $ref: "#/components/schemas/Retention" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthenticated" + "429": + $ref: "#/components/responses/RateLimited" + "500": + $ref: "#/components/responses/UnexpectedError" delete: operationId: cancelRetention tags: @@ -6196,6 +6314,10 @@ paths: Request a cancellation of a retention from the SAT. Unlike regular invoices, retention cancellations are immediate and do not require authorization from the recipient. + + If the retention status is `draft`, this method deletes it from the + database without calling SAT/PAC cancellation and without requiring + cancellation parameters. x-codeSamples: - lang: Bash label: cURL @@ -6251,7 +6373,7 @@ paths: description: ID of the retention to cancel - in: query name: motive - required: true + required: false schema: type: string enum: @@ -6260,7 +6382,8 @@ paths: - "03" - "04" description: | - Code representing the reason for the retention cancellation + Code representing the reason for the retention cancellation. + Required for retentions that are not drafts. - `01`: **Document issued with errors and replacement**. When the retention contains an error in amounts, codes, or any other data and the replacement document has already been issued, which must be indicated using the `substitution` attribute. - `02`: **Document issued with errors without replacement**. When the retention contains an error in amounts, codes, or any other data and does not need to be related to another retention. - `03`: **The operation did not take place**. When the operation or transaction was not completed. @@ -6288,6 +6411,141 @@ paths: $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" + "409": + $ref: "#/components/responses/Conflict" + "429": + $ref: "#/components/responses/RateLimited" + "500": + $ref: "#/components/responses/UnexpectedError" + /retentions/{retention_id}/copy: + post: + operationId: copyToDraftRetention + tags: + - retention + summary: Copy to draft + description: | + Creates a new draft retention with the same information as the specified + retention. The copy does not keep stamping, cancellation, idempotency, or + external identity fields. + x-codeSamples: + - lang: Bash + label: cURL + source: | + curl https://www.facturapi.io/v2/retentions/6062d9fb226600001cd22f71/copy \ + -H "Authorization: Bearer sk_test_API_KEY" \ + -X POST + - lang: JavaScript + label: Node.js + source: | + import Facturapi from 'facturapi' + const facturapi = new Facturapi('sk_test_API_KEY'); + const retention = await facturapi.retentions.copyToDraft('6062d9fb226600001cd22f71'); + - lang: csharp + label: C# + source: | + var facturapi = new FacturapiClient("sk_test_API_KEY"); + var retention = await facturapi.Retention.CopyToDraftAsync("6062d9fb226600001cd22f71"); + - lang: Java + label: Java + source: | + import io.facturapi.Facturapi; + + Facturapi facturapi = new Facturapi("sk_test_API_KEY"); + + var retention = facturapi.retentions().copyToDraft("ret_123"); + - lang: PHP + source: | + $facturapi = new Facturapi("sk_test_API_KEY"); + $retention = $facturapi->Retentions->copyToDraft("6062d9fb226600001cd22f71"); + parameters: + - in: path + name: retention_id + schema: + type: string + required: true + description: ID of the retention to copy + security: + - "SecretLiveKey": [] + - "SecretTestKey": [] + responses: + "200": + description: New `Retention` object with `draft` status. + content: + application/json: + schema: + $ref: "#/components/schemas/Retention" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthenticated" + "429": + $ref: "#/components/responses/RateLimited" + "500": + $ref: "#/components/responses/UnexpectedError" + /retentions/{retention_id}/stamp: + post: + operationId: stampDraftRetention + tags: + - retention + summary: Stamp draft retention + description: | + Stamps a draft retention and sends it to the SAT for validation. + + Facturapi validates the draft as a complete retention before stamping it. + If the draft is incomplete or invalid, the call returns an error. + x-codeSamples: + - lang: Bash + label: cURL + source: | + curl https://www.facturapi.io/v2/retentions/6062d9fb226600001cd22f71/stamp \ + -H "Authorization: Bearer sk_test_API_KEY" \ + -X POST + - lang: JavaScript + label: Node.js + source: | + import Facturapi from 'facturapi' + const facturapi = new Facturapi('sk_test_API_KEY'); + const retention = await facturapi.retentions.stampDraft('6062d9fb226600001cd22f71'); + - lang: csharp + label: C# + source: | + var facturapi = new FacturapiClient("sk_test_API_KEY"); + var retention = await facturapi.Retention.StampDraftAsync("6062d9fb226600001cd22f71"); + - lang: Java + label: Java + source: | + import io.facturapi.Facturapi; + + Facturapi facturapi = new Facturapi("sk_test_API_KEY"); + + var retention = facturapi.retentions().stampDraft("ret_123"); + - lang: PHP + source: | + $facturapi = new Facturapi("sk_test_API_KEY"); + $retention = $facturapi->Retentions->stampDraft("6062d9fb226600001cd22f71"); + parameters: + - in: path + name: retention_id + schema: + type: string + required: true + description: ID of the retention to stamp + security: + - "SecretLiveKey": [] + - "SecretTestKey": [] + responses: + "200": + description: "`Retention` object stamped successfully" + content: + application/json: + schema: + $ref: "#/components/schemas/Retention" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthenticated" + "409": + $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/RateLimited" "500": @@ -16282,6 +16540,8 @@ components: status: type: string enum: + - draft + - pending - valid - canceled description: | @@ -16308,6 +16568,12 @@ components: $ref: "#/components/schemas/Stamp" customer: $ref: "#/components/schemas/CustomerInfo" + is_ready_to_stamp: + type: boolean + description: | + Indicates whether the retention with `draft` status is complete and ready to attempt stamping. + In a retention with any status other than `draft`, this field is always `false`. + example: false RetentionProperties: type: object properties: @@ -16446,14 +16712,20 @@ components: $ref: "#/components/schemas/Retention" RetentionInput: type: object - required: - - customer - - cve_retenc - - periodo - - totales properties: + status: + type: string + enum: + - draft + description: | + Initial status of the retention. If `draft` is sent, the retention + will be saved as a draft and will not be stamped or sent to the SAT. + When `draft` is sent, `customer`, `cve_retenc`, `periodo`, and + `totales` may be omitted or sent as `null`. + example: draft customer: description: Customer receiving the invoice. + nullable: true oneOf: - $ref: "#/components/schemas/CustomerCreateInput" - type: string @@ -16462,6 +16734,7 @@ components: example: 58e93bd8e86eb318b0197456 cve_retenc: type: string + nullable: true example: 26 description: Key of the retention or payment information according to the [SAT catalog](#clave-de-retencion). fecha_exp: @@ -16479,6 +16752,7 @@ components: description: Alphanumeric identifier for internal control of the company and without fiscal relevance. periodo: type: object + nullable: true description: Information about the retention period. required: - mes_ini @@ -16503,6 +16777,7 @@ components: description: Fiscal year in which the retention was made. totales: type: object + nullable: true description: Information about the total of retentions made in the corresponding period. required: - monto_tot_operacion diff --git a/website/openapi_v2.yaml b/website/openapi_v2.yaml index fd9a7ec0..ace697dc 100644 --- a/website/openapi_v2.yaml +++ b/website/openapi_v2.yaml @@ -6038,6 +6038,11 @@ paths: summary: Crear retención description: | Crea una nueva Retención. Si el comprobante es creado en ambiente Live, ésta será **timbrado y enviado al SAT**. + + Para crear una retención en borrador, envía `status: "draft"`. En ese caso, + la retención se guardará sin timbrarse, no se enviará al PAC y podrá estar + incompleta. Facturapi asignará `is_ready_to_stamp: true` únicamente cuando + el borrador tenga todos los datos requeridos para timbrarse. x-codeSamples: - lang: Bash label: cURL @@ -6172,7 +6177,13 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/Retention" + type: object + discriminator: + propertyName: status + mapping: + valid: "#/components/schemas/Retention" + pending: "#/components/schemas/Retention" + draft: "#/components/schemas/Retention" "400": $ref: "#/components/responses/BadRequest" "401": @@ -6314,6 +6325,25 @@ paths: schema: type: string description: Identificador del cliente. Útil para obtener las retenciones emitidas a un sólo cliente. + - in: query + name: status + schema: + oneOf: + - type: string + enum: + - draft + - pending + - valid + - canceled + - type: array + items: + type: string + enum: + - draft + - pending + - valid + - canceled + description: Filtrar por uno o más estados de retención. - $ref: "#/components/parameters/SearchDate" - $ref: "#/components/parameters/SearchPage" - $ref: "#/components/parameters/SearchLimit" @@ -6402,6 +6432,95 @@ paths: $ref: "#/components/responses/RateLimited" "500": $ref: "#/components/responses/UnexpectedError" + put: + operationId: updateDraftRetention + tags: + - retention + summary: Editar borrador de retención + description: | + Actualiza la información de una retención con status `draft`, asignando + los valores de los parámetros enviados. Los parámetros que no se envíen + en la petición no se modificarán. + + Facturapi recalculará automáticamente `is_ready_to_stamp` después de cada + edición. Si la retención ya no está en status `draft`, la llamada regresará + un error. + x-codeSamples: + - lang: Bash + label: cURL + source: | + curl https://www.facturapi.io/v2/retentions/6062d9fb226600001cd22f71 \ + -X PUT \ + -H "Authorization: Bearer sk_test_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "folio_int": "R-2026-001" + }' + - lang: JavaScript + label: Node.js + source: | + import Facturapi from 'facturapi' + const facturapi = new Facturapi('sk_test_API_KEY'); + const retention = await facturapi.retentions.updateDraft( + '6062d9fb226600001cd22f71', + { folio_int: 'R-2026-001' } + ); + - lang: csharp + label: C# + source: | + var facturapi = new FacturapiClient("sk_test_API_KEY"); + var retention = await facturapi.Retention.UpdateDraftAsync( + "6062d9fb226600001cd22f71", + new Dictionary + { + ["folio_int"] = "R-2026-001" + } + ); + - lang: Java + label: Java + source: | + import io.facturapi.Facturapi; + import java.util.Map; + + Facturapi facturapi = new Facturapi("sk_test_API_KEY"); + + var retention = facturapi.retentions().updateDraft( + "ret_123", + Map.of("folio_int", "R-2026-001") + ); + - lang: PHP + source: | + $facturapi = new Facturapi("sk_test_API_KEY"); + $retention = $facturapi->Retentions->updateDraft("6062d9fb226600001cd22f71", [ + "folio_int" => "R-2026-001" + ]); + parameters: + - in: path + name: retention_id + schema: + type: string + required: true + description: ID de la retención a editar + requestBody: + $ref: "#/components/requestBodies/RetentionCreate" + security: + - "SecretLiveKey": [] + - "SecretTestKey": [] + responses: + "200": + description: Objeto `Retention` editado correctamente + content: + application/json: + schema: + $ref: "#/components/schemas/Retention" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthenticated" + "429": + $ref: "#/components/responses/RateLimited" + "500": + $ref: "#/components/responses/UnexpectedError" delete: operationId: cancelRetention tags: @@ -6411,6 +6530,9 @@ paths: Realiza una solicitud de cancelación de retención ante el SAT. A diferencia de las facturas comúnes, la cancelación de la retención es inmediata y no requiere autorización de parte del receptor. + + Si el status de la retención es `draft`, este método la eliminará de la + base de datos sin llamar al SAT/PAC y sin requerir parámetros de cancelación. x-codeSamples: - lang: Bash label: cURL @@ -6466,7 +6588,7 @@ paths: description: ID de la retención a cancelar - in: query name: motive - required: true + required: false schema: type: string enum: @@ -6475,7 +6597,8 @@ paths: - "03" - "04" description: | - Clave que representa el motivo de la cancelación de la retención + Clave que representa el motivo de la cancelación de la retención. + Requerido para retenciones que no son borrador. - `01`: **Comprobante emitido con errores con relación**. Cuando la retención contiene algún error en las cantidades, claves o cualquier otro dato y ya se ha emitido el comprobante que la sustituye, el cual deberá indicarse por medio @@ -6508,6 +6631,140 @@ paths: $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthenticated" + "409": + $ref: "#/components/responses/Conflict" + "429": + $ref: "#/components/responses/RateLimited" + "500": + $ref: "#/components/responses/UnexpectedError" + /retentions/{retention_id}/copy: + post: + operationId: copyToDraftRetention + tags: + - retention + summary: Copiar a borrador + description: | + Crea una copia en borrador de la retención especificada. La copia no conserva + campos propios del timbrado, cancelación, idempotencia o identidad externa. + x-codeSamples: + - lang: Bash + label: cURL + source: | + curl https://www.facturapi.io/v2/retentions/6062d9fb226600001cd22f71/copy \ + -H "Authorization: Bearer sk_test_API_KEY" \ + -X POST + - lang: JavaScript + label: Node.js + source: | + import Facturapi from 'facturapi' + const facturapi = new Facturapi('sk_test_API_KEY'); + const retention = await facturapi.retentions.copyToDraft('6062d9fb226600001cd22f71'); + - lang: csharp + label: C# + source: | + var facturapi = new FacturapiClient("sk_test_API_KEY"); + var retention = await facturapi.Retention.CopyToDraftAsync("6062d9fb226600001cd22f71"); + - lang: Java + label: Java + source: | + import io.facturapi.Facturapi; + + Facturapi facturapi = new Facturapi("sk_test_API_KEY"); + + var retention = facturapi.retentions().copyToDraft("ret_123"); + - lang: PHP + source: | + $facturapi = new Facturapi("sk_test_API_KEY"); + $retention = $facturapi->Retentions->copyToDraft("6062d9fb226600001cd22f71"); + parameters: + - in: path + name: retention_id + schema: + type: string + required: true + description: ID de la retención a copiar + security: + - "SecretLiveKey": [] + - "SecretTestKey": [] + responses: + "200": + description: Nuevo objeto `Retention` con status `draft`. + content: + application/json: + schema: + $ref: "#/components/schemas/Retention" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthenticated" + "429": + $ref: "#/components/responses/RateLimited" + "500": + $ref: "#/components/responses/UnexpectedError" + /retentions/{retention_id}/stamp: + post: + operationId: stampDraftRetention + tags: + - retention + summary: Timbrar borrador de retención + description: | + Timbra una retención con status `draft` y la envía al SAT para su validación. + + Facturapi validará el borrador como una retención completa antes de timbrarlo. + Si el borrador está incompleto o no es válido, la llamada regresará un error. + x-codeSamples: + - lang: Bash + label: cURL + source: | + curl https://www.facturapi.io/v2/retentions/6062d9fb226600001cd22f71/stamp \ + -H "Authorization: Bearer sk_test_API_KEY" \ + -X POST + - lang: JavaScript + label: Node.js + source: | + import Facturapi from 'facturapi' + const facturapi = new Facturapi('sk_test_API_KEY'); + const retention = await facturapi.retentions.stampDraft('6062d9fb226600001cd22f71'); + - lang: csharp + label: C# + source: | + var facturapi = new FacturapiClient("sk_test_API_KEY"); + var retention = await facturapi.Retention.StampDraftAsync("6062d9fb226600001cd22f71"); + - lang: Java + label: Java + source: | + import io.facturapi.Facturapi; + + Facturapi facturapi = new Facturapi("sk_test_API_KEY"); + + var retention = facturapi.retentions().stampDraft("ret_123"); + - lang: PHP + source: | + $facturapi = new Facturapi("sk_test_API_KEY"); + $retention = $facturapi->Retentions->stampDraft("6062d9fb226600001cd22f71"); + parameters: + - in: path + name: retention_id + schema: + type: string + required: true + description: ID de la retención a timbrar + security: + - "SecretLiveKey": [] + - "SecretTestKey": [] + responses: + "200": + description: Objeto `Retention` timbrado correctamente + content: + application/json: + schema: + $ref: "#/components/schemas/Retention" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthenticated" + "409": + $ref: "#/components/responses/Conflict" "429": $ref: "#/components/responses/RateLimited" "500": @@ -16535,6 +16792,8 @@ components: status: type: string enum: + - draft + - pending - valid - canceled description: | @@ -16560,6 +16819,12 @@ components: $ref: "#/components/schemas/Stamp" customer: $ref: "#/components/schemas/CustomerInfo" + is_ready_to_stamp: + type: boolean + description: | + Indica si la retención con status `draft` está completa y lista para intentar timbrarse. + En una retención con status diferente a `draft`, este campo siempre será `false`. + example: false RetentionProperties: type: object properties: @@ -16688,14 +16953,20 @@ components: $ref: "#/components/schemas/Retention" RetentionInput: type: object - required: - - customer - - cve_retenc - - periodo - - totales properties: + status: + type: string + enum: + - draft + description: | + Estado inicial de la retención. Si se envía `draft`, la retención se + guardará como borrador y no se timbrará ni se enviará al SAT. También + al enviar `draft`, `customer`, `cve_retenc`, `periodo` y `totales` + pueden omitirse o enviarse como `null`. + example: draft customer: description: Cliente receptor de la factura. + nullable: true oneOf: - $ref: "#/components/schemas/CustomerCreateInput" - type: string @@ -16704,6 +16975,7 @@ components: example: 58e93bd8e86eb318b0197456 cve_retenc: type: string + nullable: true example: 26 description: Clave de la retención o información de pagos de acuerdo al [catálogo del SAT](#clave-de-retencion). fecha_exp: @@ -16720,6 +16992,7 @@ components: description: Identificador alfanumérico para control interno de la empresa y sin relevancia fiscal. periodo: type: object + nullable: true description: Información sobre el periodo de la retención. required: - mes_ini @@ -16744,6 +17017,7 @@ components: description: Año o ejercicio fiscal en que se realizó la retención. totales: type: object + nullable: true description: Información sobre el total de retenciones efectuadas en el periodo correspondiente. required: - monto_tot_operacion