{
  "openapi": "3.0.3",
  "info": {
    "title": "Partner API — Firma Digital",
    "version": "1.0.0",
    "description": "Redeem purchase codes, retrieve the issued certificate, and check status, for partners integrated with Firma Digital. Replaces the previous v2/v3 versions: a single redeem flow whose behavior (required fields, whether it validates against Clave Única or only against the Chilean civil registry) adapts automatically to the partner's configuration.\n\nThis is v1: the version lives in the URL (`/api/partners/v1`). A future v2 would coexist as a separate section, without breaking this one."
  },
  "servers": [
    { "url": "https://dev-api.tsp.cl/firma_digital/dev/api/partners/v1", "description": "Development" },
    { "url": "https://api.firma.digital/api/partners/v1", "description": "Production" }
  ],
  "components": {
    "securitySchemes": {
      "PartnerApiKey": {
        "type": "apiKey",
        "in": "header",
        "name": "x-gw",
        "description": "Partner API Key, issued when the integration is set up."
      }
    },
    "schemas": {
      "ErrorResponse": {
        "type": "object",
        "properties": {
          "success": { "type": "boolean", "example": false },
          "error": { "type": "string", "example": "VALIDATION_ERROR" },
          "message": { "type": "string" }
        }
      },
      "Titular": {
        "type": "object",
        "properties": {
          "rut": { "type": "string", "example": "18925910-7" },
          "nombreCompleto": { "type": "string", "example": "FABIAN JESUS AGUILAR ALISTE" },
          "email": { "type": "string", "format": "email" }
        }
      }
    }
  },
  "security": [{ "PartnerApiKey": [] }],
  "paths": {
    "/certificate_request": {
      "post": {
        "summary": "Redeem a purchase code (validate identity)",
        "description": "Validates the identity of the holder of a purchase code already issued (via `POST /pcode` on the `partner-pcode` Worker). It does not deliver the certificate in this call — that's what `certificate_claim` is for — unless the partner is configured in B2B mode, in which case the certificate is generated and delivered automatically (webhook and/or email to the partner) in this same call, with no separate claim step for the end user.\n\nWhich fields are required depends on `partner_config.require_cedula_serie`, not on this call: a partner with the reduced flow (`require_cedula_serie=1`) must not send `names`/`fLastname`/`phone` (names come from the civil registry) but must send `serialNumber`; a partner with the full flow must send `names`/`fLastname`/`phone` and additionally validates against Clave Única.",
        "operationId": "certificateRequest",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["pCode", "referenceId", "dni", "email", "pin"],
                "properties": {
                  "pCode": { "type": "string", "description": "Purchase code previously generated for this partner." },
                  "referenceId": { "type": "string", "description": "The partner's own identifier for this transaction." },
                  "dni": { "type": "string", "example": "18925910-7", "description": "Holder's RUT (Chilean national ID), with check digit." },
                  "email": { "type": "string", "format": "email" },
                  "pin": { "type": "string", "pattern": "^\\d{4}$", "description": "4 digits — doubles as the resulting PFX's password." },
                  "serialNumber": { "type": "string", "description": "ID card serial number. Required if the partner has the reduced flow (`require_cedula_serie=1`)." },
                  "names": { "type": "string", "description": "Holder's first name(s). Required only in the full flow." },
                  "fLastname": { "type": "string", "description": "Paternal last name. Required only in the full flow." },
                  "mLastname": { "type": "string" },
                  "phone": { "type": "string", "example": "+56911111111", "description": "Required only in the full flow." },
                  "claveUnica": { "type": "string", "description": "Clave Única token, if the partner's flow requires it." },
                  "dniFront": { "type": "string" },
                  "dniBack": { "type": "string" },
                  "partnerCopy": { "type": "string", "format": "email", "description": "Extra email address to CC the certificate to (full flow)." },
                  "fechaVencimiento": {
                    "type": "string",
                    "description": "ID card expiration date as entered by the user, `YYYY-MM-DD` or `DD-MM-YYYY`/`DD/MM/YYYY` (day first). Compared against the real civil registry date if the partner has `validate_vencimiento=1`.",
                    "example": "21/09/2034"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Identity validated. `status` indicates whether the certificate still needs to be claimed (`VALIDATED`, B2C flow) or was already issued and delivered (`EMITTED`, B2B flow).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "type": "boolean", "example": true },
                    "data": {
                      "type": "object",
                      "properties": {
                        "p_code": { "type": "string" },
                        "referenceId": { "type": "string" },
                        "status": { "type": "string", "enum": ["VALIDATED", "EMITTED"] },
                        "titular": { "$ref": "#/components/schemas/Titular" },
                        "certificate": {
                          "type": "object",
                          "description": "Only present when `status` is `EMITTED` (B2B partner).",
                          "properties": {
                            "serialNumber": { "type": "string" },
                            "validFrom": { "type": "string", "format": "date-time" },
                            "validTo": { "type": "string", "format": "date-time" },
                            "pfxFileName": { "type": "string" }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": { "description": "Missing required field or invalid format.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "401": { "description": "Invalid or missing API Key." },
          "404": { "description": "The purchase code does not exist." },
          "406": {
            "description": "Business rejection: required serial number missing, RUT does not match the purchase code, email already used by another holder, another purchase code has an ongoing validation, ID card does not match the civil registry, or expiration date does not match.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
          },
          "423": { "description": "The purchase code is permanently blocked." },
          "429": {
            "description": "ID validation attempt limit reached (security lock), or the emission rate limit was exceeded (10 requests/minute per partner)."
          }
        }
      }
    },
    "/certificate_claim": {
      "post": {
        "summary": "Retrieve the certificate (PFX) — first time only",
        "description": "Generates and delivers the certificate for a purchase code that already went through `certificate_request` (B2C flow). It is one-time only: if the certificate was already claimed, it responds `201` without regenerating it. Does not apply to B2B partners, who receive the certificate automatically during `certificate_request`.",
        "operationId": "certificateClaim",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["p_code", "dni", "pin"],
                "properties": {
                  "p_code": { "type": "string" },
                  "dni": { "type": "string", "example": "18925910-7" },
                  "pin": { "type": "string", "pattern": "^\\d{4}$", "description": "The same PIN sent in certificate_request." }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Certificate issued and delivered.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "type": "boolean", "example": true },
                    "data": {
                      "type": "object",
                      "properties": {
                        "p_code": { "type": "string" },
                        "certificate": { "type": "string", "format": "byte", "description": "Base64-encoded PFX." },
                        "validFrom": { "type": "string", "format": "date-time" },
                        "validTo": { "type": "string", "format": "date-time" },
                        "serialNumber": { "type": "string" }
                      }
                    }
                  }
                }
              }
            }
          },
          "201": { "description": "The certificate was already claimed before — not regenerated. `error: \"ALREADY_ISSUED\"`." },
          "202": { "description": "A validation is currently in progress for this purchase code — retry in a few seconds." },
          "401": { "description": "Invalid or missing API Key." },
          "404": { "description": "The purchase code does not exist, or exists but hasn't gone through `certificate_request` yet (identity not validated)." },
          "406": { "description": "RUT/PIN do not match what was set during `certificate_request`." }
        }
      }
    },
    "/status": {
      "get": {
        "summary": "Check a purchase code's status",
        "description": "A simplified status, without exposing internal fields — meant for a partner integrated via the widget to poll how far along a user is, without relying on listening to widget events in real time.",
        "operationId": "getStatus",
        "parameters": [
          { "name": "p_code", "in": "query", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "type": "boolean", "example": true },
                    "data": {
                      "type": "object",
                      "properties": {
                        "p_code": { "type": "string" },
                        "status": {
                          "type": "string",
                          "enum": ["NOT_FOUND", "ACTIVE", "VALIDATED", "IN_ONBOARDING", "EMITTED", "EXPIRED", "LOCKED", "CANCELLED"],
                          "description": "In precedence order (the first one that applies wins): CANCELLED (voided, wins even over an already-issued certificate) > EXPIRED (issued but has since expired) > EMITTED (certificate delivered) > LOCKED (blocked before it got issued) > IN_ONBOARDING (midway through the widget flow) > VALIDATED (identity verified, certificate_claim still pending, B2C only) > ACTIVE (exists, nobody has redeemed it) > NOT_FOUND (doesn't exist). VALIDATED and IN_ONBOARDING aren't yet part of the purchase_codes.status enum in the unified data model this is migrating toward."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": { "description": "Missing p_code parameter." },
          "401": { "description": "Invalid or missing API Key." }
        }
      }
    }
  }
}
