# Contrato.Cloud KYC

Revision: ab4c5635b7f0f446ba6cfcf3607f9fb94636e233074478aed2ea1a9c23f83ead

# API KYC: de la identificación al resultado

Source: https://contrato.cloud/es/desarrolladores/kyc/vision-general

## Un flujo, tres responsabilidades

Su servidor crea una transacción mediante la API. La persona realiza la verificación en el recorrido alojado, al que accede mediante el enlace devuelto. Su sistema consulta el resultado y recibe eventos para continuar el proceso de negocio.

1. **Crear:** utilice `POST /v1/face-auth/transactions` con `X-API-Key` desde su backend.
2. **Derivar:** utilice la URL devuelta; la clave nunca debe llegar al navegador de la persona.
3. **Acompañar:** vincule `transaction_id` a su proceso, valide webhooks y concilie el estado mediante GET.

## Qué representa el resultado

La verificación de identidad es una etapa de su flujo. Una identidad aprobada no aprueba automáticamente crédito, una compra, una cuenta ni facultades de representación. Su sistema conserva la responsabilidad sobre las reglas comerciales, los casos pendientes y la decisión de continuar.

`status` describe la etapa operativa; `decision` indica `pending`, `approved` o `rejected`. No convierta una etapa intermedia en aprobación. Los recorridos vencidos también requieren un tratamiento específico.

## Elija cómo empezar

Utilice el [simulador local](/es/desarrolladores/kyc/simulador) para comprender el flujo sin credenciales ni datos personales. Para integrar su backend, siga el [inicio rápido](/es/desarrolladores/kyc/inicio-rapido) y consulte la [referencia](/es/desarrolladores/kyc/referencia).

Los ejemplos utilizan datos sintéticos y una dirección reservada para documentación. La URL de API, las credenciales y los accesos de su entorno se definen al habilitar la integración.


---

# Integre su primer recorrido KYC

Source: https://contrato.cloud/es/desarrolladores/kyc/inicio-rapido

## Antes de ejecutar

Los comandos son ejemplos sintéticos. `api.example.invalid` no es un entorno de pruebas ni resuelve a un servicio real. Sustituya la base únicamente por la dirección confirmada para su entorno habilitado y mantenga la clave en el backend. Para explorar sin red ni credenciales, abra el [simulador](/es/desarrolladores/kyc/simulador).

## 1. Cree una transacción

Elija una clave de idempotencia estable para el mismo intento de creación. El CPF siguiente es un valor ficticio de documentación.

Crear transacción KYC

### cURL

```bash
# Server-side only. Configure KYC_API_BASE_URL with your authorized base (https://api.example.invalid is illustrative), and KYC_API_KEY privately. Never expose credentials in browser code.
set -eu
: "${KYC_API_BASE_URL:?Set authorized base}" "${KYC_API_KEY:?Set API key}"
: "${KYC_IDEMPOTENCY_KEY:?Persist the key for this creation intent}"
curl --fail-with-body --max-time 15 --request POST \
  "${KYC_API_BASE_URL%/}/v1/face-auth/transactions" \
  --header "X-API-Key: ${KYC_API_KEY}" \
  --header "Idempotency-Key: ${KYC_IDEMPOTENCY_KEY}" \
  --header 'Content-Type: application/json' \
  --data-binary '{"country_code":"BR","external_id":"docs-example-0001","locale":"pt-BR","subject":{"identity":{"country":"BR","number":"00000000000","type":"CPF"}}}'
# Persist transaction_id and business correlation; use the returned links.webview_url. Read status, decision and decision_reason together. Reuse the SAME idempotency key AND original payload only for the same creation intent; never silently bypass a 409 conflict.
```

### JavaScript / Node.js

```javascript
import process from "node:process";
// Server-side only. Configure KYC_API_BASE_URL with your authorized base (https://api.example.invalid is illustrative), and KYC_API_KEY privately. Never expose credentials in browser code.
function required(name) {
  const value = process.env[name];
  if (!value) throw new Error("Missing configuration: " + name);
  return value;
}
const base = required("KYC_API_BASE_URL").replace(/\/$/, "");
const response = await fetch(base + "/v1/face-auth/transactions", {
  method: "POST",
  headers: { "X-API-Key": required("KYC_API_KEY"), "Content-Type": "application/json", "Idempotency-Key": required("KYC_IDEMPOTENCY_KEY") },
  body: "{\"country_code\":\"BR\",\"external_id\":\"docs-example-0001\",\"locale\":\"pt-BR\",\"subject\":{\"identity\":{\"country\":\"BR\",\"number\":\"00000000000\",\"type\":\"CPF\"}}}",
  signal: AbortSignal.timeout(15_000),
});
if (response.status !== 201) throw new Error("KYC HTTP " + response.status);
const transaction = await response.json();
console.log("HTTP " + response.status);
// Persist transaction_id and business correlation; use the returned links.webview_url. Read status, decision and decision_reason together. Reuse the SAME idempotency key AND original payload only for the same creation intent; never silently bypass a 409 conflict.
```

### TypeScript / Node.js

```typescript
import process from "node:process";
// Server-side only. Configure KYC_API_BASE_URL with your authorized base (https://api.example.invalid is illustrative), and KYC_API_KEY privately. Never expose credentials in browser code.
function required(name: string): string {
  const value = process.env[name];
  if (!value) throw new Error("Missing configuration: " + name);
  return value;
}
const base = required("KYC_API_BASE_URL").replace(/\/$/, "");
const response = await fetch(base + "/v1/face-auth/transactions", {
  method: "POST",
  headers: { "X-API-Key": required("KYC_API_KEY"), "Content-Type": "application/json", "Idempotency-Key": required("KYC_IDEMPOTENCY_KEY") },
  body: "{\"country_code\":\"BR\",\"external_id\":\"docs-example-0001\",\"locale\":\"pt-BR\",\"subject\":{\"identity\":{\"country\":\"BR\",\"number\":\"00000000000\",\"type\":\"CPF\"}}}",
  signal: AbortSignal.timeout(15_000),
});
if (response.status !== 201) throw new Error("KYC HTTP " + response.status);
const transaction: unknown = await response.json();
// Validate the response against the curated OpenAPI before using it.
console.log("HTTP " + response.status);
// Persist transaction_id and business correlation; use the returned links.webview_url. Read status, decision and decision_reason together. Reuse the SAME idempotency key AND original payload only for the same creation intent; never silently bypass a 409 conflict.
```

### Python

```python
import json
import os
from urllib.parse import quote
from urllib.request import Request, urlopen
# Server-side only. Configure KYC_API_BASE_URL with your authorized base (https://api.example.invalid is illustrative), and KYC_API_KEY privately. Never expose credentials in browser code.
base = os.environ["KYC_API_BASE_URL"].rstrip("/")
request = Request(base + "/v1/face-auth/transactions",
    method="POST",
    headers={"X-API-Key": os.environ["KYC_API_KEY"], "Content-Type": "application/json",
             "Idempotency-Key": os.environ["KYC_IDEMPOTENCY_KEY"]},
    data="{\"country_code\":\"BR\",\"external_id\":\"docs-example-0001\",\"locale\":\"pt-BR\",\"subject\":{\"identity\":{\"country\":\"BR\",\"number\":\"00000000000\",\"type\":\"CPF\"}}}".encode("utf-8"),
)
with urlopen(request, timeout=15) as response:
    if response.status != 201:
        raise RuntimeError("KYC HTTP " + str(response.status))
    transaction = json.load(response)
    print("HTTP " + str(response.status))
# Persist transaction_id and business correlation; use the returned links.webview_url. Read status, decision and decision_reason together. Reuse the SAME idempotency key AND original payload only for the same creation intent; never silently bypass a 409 conflict.
```

### PHP

```php
<?php
// PHP 8+, standard HTTP streams. Server-side only. Configure KYC_API_BASE_URL with your authorized base (https://api.example.invalid is illustrative), and KYC_API_KEY privately. Never expose credentials in browser code.
function required(string $name): string {
    $value = getenv($name);
    if ($value === false || $value === '') throw new RuntimeException("Missing configuration: " . $name);
    return $value;
}
$url = rtrim(required('KYC_API_BASE_URL'), '/') . '/v1/face-auth/transactions';
$headers = ['X-API-Key: ' . required('KYC_API_KEY'), 'Content-Type: application/json', 'Idempotency-Key: ' . required('KYC_IDEMPOTENCY_KEY')];
$context = stream_context_create(['http' => ['method' => 'POST', 'header' => implode("\r\n", $headers),
    'timeout' => 15, 'ignore_errors' => true, 'content' => '{"country_code":"BR","external_id":"docs-example-0001","locale":"pt-BR","subject":{"identity":{"country":"BR","number":"00000000000","type":"CPF"}}}']]);
$body = file_get_contents($url, false, $context);
if ($body === false) throw new RuntimeException('KYC transport failure');
if (!preg_match('/^HTTP\/\S+ ([0-9]{3})/', $http_response_header[0] ?? '', $matches)) throw new RuntimeException('Invalid HTTP response');
$status = (int) $matches[1];
if ($status !== 201) throw new RuntimeException('KYC HTTP ' . $status);
$transaction = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
echo 'HTTP ' . $status . PHP_EOL;
// Persist transaction_id and business correlation; use the returned links.webview_url. Read status, decision and decision_reason together. Reuse the SAME idempotency key AND original payload only for the same creation intent; never silently bypass a 409 conflict.
```

### C# / .NET

```csharp
using System;
using System.Net.Http;
using System.Text;
using System.Text.Json;
// .NET 8+. Server-side only. Configure KYC_API_BASE_URL with your authorized base (https://api.example.invalid is illustrative), and KYC_API_KEY privately. Never expose credentials in browser code.
static string Required(string name) => Environment.GetEnvironmentVariable(name) is { Length: > 0 } value ? value : throw new Exception("Missing configuration: " + name);
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(15) };
using var request = new HttpRequestMessage(HttpMethod.Post, Required("KYC_API_BASE_URL").TrimEnd('/') + "/v1/face-auth/transactions");
request.Headers.Add("X-API-Key", Required("KYC_API_KEY"));
request.Headers.Add("Idempotency-Key", Required("KYC_IDEMPOTENCY_KEY"));
request.Content = new StringContent("{\"country_code\":\"BR\",\"external_id\":\"docs-example-0001\",\"locale\":\"pt-BR\",\"subject\":{\"identity\":{\"country\":\"BR\",\"number\":\"00000000000\",\"type\":\"CPF\"}}}", Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
if ((int)response.StatusCode != 201) throw new Exception("KYC HTTP " + (int)response.StatusCode);
using var transaction = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
Console.WriteLine("HTTP " + (int)response.StatusCode);
// Persist transaction_id and business correlation; use the returned links.webview_url. Read status, decision and decision_reason together. Reuse the SAME idempotency key AND original payload only for the same creation intent; never silently bypass a 409 conflict.
```

### Java

```java
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.*;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
// Java 17+. Server-side only. Configure KYC_API_BASE_URL with your authorized base (https://api.example.invalid is illustrative), and KYC_API_KEY privately. Never expose credentials in browser code.
class Example {
  static String required(String name) {
    String value = System.getenv(name);
    if (value == null || value.isEmpty()) throw new IllegalStateException("Missing configuration: " + name);
    return value;
  }
  public static void main(String[] args) throws Exception {
    String base = required("KYC_API_BASE_URL").replaceAll("/+$", "");
    String url = base + "/v1/face-auth/transactions";
    var builder = HttpRequest.newBuilder(URI.create(url)).timeout(Duration.ofSeconds(15))
        .header("X-API-Key", required("KYC_API_KEY"));
    builder.header("Idempotency-Key", required("KYC_IDEMPOTENCY_KEY"))
        .header("Content-Type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString("{\"country_code\":\"BR\",\"external_id\":\"docs-example-0001\",\"locale\":\"pt-BR\",\"subject\":{\"identity\":{\"country\":\"BR\",\"number\":\"00000000000\",\"type\":\"CPF\"}}}", StandardCharsets.UTF_8));
    var response = HttpClient.newHttpClient().send(builder.build(), HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
    if (response.statusCode() != 201) throw new IllegalStateException("KYC HTTP " + response.statusCode());
    System.out.println("HTTP " + response.statusCode());
    // Parse response.body() with your application's JSON parser and validate against OpenAPI.
    // Persist transaction_id and business correlation; use the returned links.webview_url. Read status, decision and decision_reason together. Reuse the SAME idempotency key AND original payload only for the same creation intent; never silently bypass a 409 conflict.
  }
}
```

### Go

```go
package main
import (
  "encoding/json"
  "fmt"
  "net/http"
  "net/url"
  "os"
  "strings"
  "time"
)
// Server-side only. Configure KYC_API_BASE_URL with your authorized base (https://api.example.invalid is illustrative), and KYC_API_KEY privately. Never expose credentials in browser code.
func required(name string) string {
  value := os.Getenv(name); if value == "" { panic("Missing configuration: " + name) }; return value
}
func main() {
  _ = url.PathEscape // Used by the GET variant.
  endpoint := strings.TrimRight(required("KYC_API_BASE_URL"), "/") + "/v1/face-auth/transactions"
  request, err := http.NewRequest("POST", endpoint, strings.NewReader(`{"country_code":"BR","external_id":"docs-example-0001","locale":"pt-BR","subject":{"identity":{"country":"BR","number":"00000000000","type":"CPF"}}}`)); if err != nil { panic(err) }
  request.Header.Set("X-API-Key", required("KYC_API_KEY"))
  request.Header.Set("Content-Type", "application/json")
  request.Header.Set("Idempotency-Key", required("KYC_IDEMPOTENCY_KEY"))
  response, err := (&http.Client{Timeout: 15*time.Second}).Do(request); if err != nil { panic(err) }; defer response.Body.Close()
  if response.StatusCode != 201 { panic(fmt.Sprintf("KYC HTTP %d", response.StatusCode)) }
  var transaction map[string]any
  if err := json.NewDecoder(response.Body).Decode(&transaction); err != nil { panic(err) }
  fmt.Printf("HTTP %d\n", response.StatusCode)
  // Persist transaction_id and business correlation; use the returned links.webview_url. Read status, decision and decision_reason together. Reuse the SAME idempotency key AND original payload only for the same creation intent; never silently bypass a 409 conflict.
}
```

## 2. Derive al recorrido

El POST devuelve `201` y el identificador `transaction_id`. Guárdelo junto a su `external_id`. Derive a la persona mediante la URL alojada devuelta en `links.webview_url`; no construya enlaces a partir del identificador de la transacción.

## 3. Consulte el resultado

Consultar y reconciliar transacción KYC

### cURL

```bash
# Server-side only. Configure KYC_API_BASE_URL with your authorized base (https://api.example.invalid is illustrative), and KYC_API_KEY privately. Never expose credentials in browser code.
set -eu
: "${KYC_API_BASE_URL:?Set authorized base}" "${KYC_API_KEY:?Set API key}"
: "${KYC_TRANSACTION_ID:?Use the returned transaction_id}"
curl --fail-with-body --max-time 15 --request GET \
  "${KYC_API_BASE_URL%/}/v1/face-auth/transactions/${KYC_TRANSACTION_ID}" \
  --header "X-API-Key: ${KYC_API_KEY}"
# Persist transaction_id and business correlation; use the returned links.webview_url. Read status, decision and decision_reason together. Reuse the SAME idempotency key AND original payload only for the same creation intent; never silently bypass a 409 conflict.
```

### JavaScript / Node.js

```javascript
import process from "node:process";
// Server-side only. Configure KYC_API_BASE_URL with your authorized base (https://api.example.invalid is illustrative), and KYC_API_KEY privately. Never expose credentials in browser code.
function required(name) {
  const value = process.env[name];
  if (!value) throw new Error("Missing configuration: " + name);
  return value;
}
const base = required("KYC_API_BASE_URL").replace(/\/$/, "");
const response = await fetch(base + "/v1/face-auth/transactions" + "/" + encodeURIComponent(required("KYC_TRANSACTION_ID")), {
  method: "GET",
  headers: { "X-API-Key": required("KYC_API_KEY") },
  signal: AbortSignal.timeout(15_000),
});
if (response.status !== 200) throw new Error("KYC HTTP " + response.status);
const transaction = await response.json();
console.log("HTTP " + response.status);
// Persist transaction_id and business correlation; use the returned links.webview_url. Read status, decision and decision_reason together. Reuse the SAME idempotency key AND original payload only for the same creation intent; never silently bypass a 409 conflict.
```

### TypeScript / Node.js

```typescript
import process from "node:process";
// Server-side only. Configure KYC_API_BASE_URL with your authorized base (https://api.example.invalid is illustrative), and KYC_API_KEY privately. Never expose credentials in browser code.
function required(name: string): string {
  const value = process.env[name];
  if (!value) throw new Error("Missing configuration: " + name);
  return value;
}
const base = required("KYC_API_BASE_URL").replace(/\/$/, "");
const response = await fetch(base + "/v1/face-auth/transactions" + "/" + encodeURIComponent(required("KYC_TRANSACTION_ID")), {
  method: "GET",
  headers: { "X-API-Key": required("KYC_API_KEY") },
  signal: AbortSignal.timeout(15_000),
});
if (response.status !== 200) throw new Error("KYC HTTP " + response.status);
const transaction: unknown = await response.json();
// Validate the response against the curated OpenAPI before using it.
console.log("HTTP " + response.status);
// Persist transaction_id and business correlation; use the returned links.webview_url. Read status, decision and decision_reason together. Reuse the SAME idempotency key AND original payload only for the same creation intent; never silently bypass a 409 conflict.
```

### Python

```python
import json
import os
from urllib.parse import quote
from urllib.request import Request, urlopen
# Server-side only. Configure KYC_API_BASE_URL with your authorized base (https://api.example.invalid is illustrative), and KYC_API_KEY privately. Never expose credentials in browser code.
base = os.environ["KYC_API_BASE_URL"].rstrip("/")
request = Request(base + "/v1/face-auth/transactions" + "/" + quote(os.environ["KYC_TRANSACTION_ID"], safe=""),
    method="GET",
    headers={"X-API-Key": os.environ["KYC_API_KEY"]},
)
with urlopen(request, timeout=15) as response:
    if response.status != 200:
        raise RuntimeError("KYC HTTP " + str(response.status))
    transaction = json.load(response)
    print("HTTP " + str(response.status))
# Persist transaction_id and business correlation; use the returned links.webview_url. Read status, decision and decision_reason together. Reuse the SAME idempotency key AND original payload only for the same creation intent; never silently bypass a 409 conflict.
```

### PHP

```php
<?php
// PHP 8+, standard HTTP streams. Server-side only. Configure KYC_API_BASE_URL with your authorized base (https://api.example.invalid is illustrative), and KYC_API_KEY privately. Never expose credentials in browser code.
function required(string $name): string {
    $value = getenv($name);
    if ($value === false || $value === '') throw new RuntimeException("Missing configuration: " . $name);
    return $value;
}
$url = rtrim(required('KYC_API_BASE_URL'), '/') . '/v1/face-auth/transactions' . '/' . rawurlencode(required('KYC_TRANSACTION_ID'));
$headers = ['X-API-Key: ' . required('KYC_API_KEY')];
$context = stream_context_create(['http' => ['method' => 'GET', 'header' => implode("\r\n", $headers),
    'timeout' => 15, 'ignore_errors' => true]]);
$body = file_get_contents($url, false, $context);
if ($body === false) throw new RuntimeException('KYC transport failure');
if (!preg_match('/^HTTP\/\S+ ([0-9]{3})/', $http_response_header[0] ?? '', $matches)) throw new RuntimeException('Invalid HTTP response');
$status = (int) $matches[1];
if ($status !== 200) throw new RuntimeException('KYC HTTP ' . $status);
$transaction = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
echo 'HTTP ' . $status . PHP_EOL;
// Persist transaction_id and business correlation; use the returned links.webview_url. Read status, decision and decision_reason together. Reuse the SAME idempotency key AND original payload only for the same creation intent; never silently bypass a 409 conflict.
```

### C# / .NET

```csharp
using System;
using System.Net.Http;
using System.Text;
using System.Text.Json;
// .NET 8+. Server-side only. Configure KYC_API_BASE_URL with your authorized base (https://api.example.invalid is illustrative), and KYC_API_KEY privately. Never expose credentials in browser code.
static string Required(string name) => Environment.GetEnvironmentVariable(name) is { Length: > 0 } value ? value : throw new Exception("Missing configuration: " + name);
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(15) };
using var request = new HttpRequestMessage(HttpMethod.Get, Required("KYC_API_BASE_URL").TrimEnd('/') + "/v1/face-auth/transactions" + "/" + Uri.EscapeDataString(Required("KYC_TRANSACTION_ID")));
request.Headers.Add("X-API-Key", Required("KYC_API_KEY"));
using var response = await client.SendAsync(request);
if ((int)response.StatusCode != 200) throw new Exception("KYC HTTP " + (int)response.StatusCode);
using var transaction = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
Console.WriteLine("HTTP " + (int)response.StatusCode);
// Persist transaction_id and business correlation; use the returned links.webview_url. Read status, decision and decision_reason together. Reuse the SAME idempotency key AND original payload only for the same creation intent; never silently bypass a 409 conflict.
```

### Java

```java
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.*;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
// Java 17+. Server-side only. Configure KYC_API_BASE_URL with your authorized base (https://api.example.invalid is illustrative), and KYC_API_KEY privately. Never expose credentials in browser code.
class Example {
  static String required(String name) {
    String value = System.getenv(name);
    if (value == null || value.isEmpty()) throw new IllegalStateException("Missing configuration: " + name);
    return value;
  }
  public static void main(String[] args) throws Exception {
    String base = required("KYC_API_BASE_URL").replaceAll("/+$", "");
    String url = base + "/v1/face-auth/transactions" + "/" + URLEncoder.encode(required("KYC_TRANSACTION_ID"), StandardCharsets.UTF_8).replace("+", "%20");
    var builder = HttpRequest.newBuilder(URI.create(url)).timeout(Duration.ofSeconds(15))
        .header("X-API-Key", required("KYC_API_KEY"));
    builder.GET();
    var response = HttpClient.newHttpClient().send(builder.build(), HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
    if (response.statusCode() != 200) throw new IllegalStateException("KYC HTTP " + response.statusCode());
    System.out.println("HTTP " + response.statusCode());
    // Parse response.body() with your application's JSON parser and validate against OpenAPI.
    // Persist transaction_id and business correlation; use the returned links.webview_url. Read status, decision and decision_reason together. Reuse the SAME idempotency key AND original payload only for the same creation intent; never silently bypass a 409 conflict.
  }
}
```

### Go

```go
package main
import (
  "encoding/json"
  "fmt"
  "net/http"
  "net/url"
  "os"
  "strings"
  "time"
)
// Server-side only. Configure KYC_API_BASE_URL with your authorized base (https://api.example.invalid is illustrative), and KYC_API_KEY privately. Never expose credentials in browser code.
func required(name string) string {
  value := os.Getenv(name); if value == "" { panic("Missing configuration: " + name) }; return value
}
func main() {
  _ = url.PathEscape // Used by the GET variant.
  endpoint := strings.TrimRight(required("KYC_API_BASE_URL"), "/") + "/v1/face-auth/transactions" + "/" + url.PathEscape(required("KYC_TRANSACTION_ID"))
  request, err := http.NewRequest("GET", endpoint, nil); if err != nil { panic(err) }
  request.Header.Set("X-API-Key", required("KYC_API_KEY"))
  response, err := (&http.Client{Timeout: 15*time.Second}).Do(request); if err != nil { panic(err) }; defer response.Body.Close()
  if response.StatusCode != 200 { panic(fmt.Sprintf("KYC HTTP %d", response.StatusCode)) }
  var transaction map[string]any
  if err := json.NewDecoder(response.Body).Decode(&transaction); err != nil { panic(err) }
  fmt.Printf("HTTP %d\n", response.StatusCode)
  // Persist transaction_id and business correlation; use the returned links.webview_url. Read status, decision and decision_reason together. Reuse the SAME idempotency key AND original payload only for the same creation intent; never silently bypass a 409 conflict.
}
```

Lea `status`, `decision` y `decision_reason` en conjunto. Una decisión `pending` todavía no permite concluir el flujo como aprobado. La consulta requiere una clave del mismo Tenant y entorno que la transacción.

## Siguiente paso

Implemente [webhooks](/es/desarrolladores/kyc/webhooks-y-eventos) con [validación HMAC](/es/desarrolladores/kyc/validar-firma-hmac) y [conciliación](/es/desarrolladores/kyc/reentrega-y-conciliacion). Antes de producción, cubra rechazo, vencimiento, revisión manual, duplicados y pérdida temporal de conexión.


---

# Integre KYC con su agente de IA

Source: https://contrato.cloud/es/desarrolladores/integrar-con-ia

## Prepare la integración KYC con su agente

Elija el lenguaje y copie las instrucciones siguientes en su agente de desarrollo. El contexto incluye las guías públicas, ejemplos completos y la referencia del contrato. Prepare con datos sintéticos; la base de ejemplo `.invalid` no es un entorno de ejecución.

## Prompt y contexto de integración

### cURL

```text
Implemente una integración backend de KYC de Contrato.Cloud en cURL, utilizando solo la documentación pública indicada abajo.
1. Lea el contexto y OpenAPI antes de escribir código. Cite las páginas utilizadas; pida aclaraciones cuando un campo o comportamiento no esté documentado. No invente una API de firmas SIGN, rutas Admin o endpoints privados del Portal.
2. Use https://api.example.invalid como base sintética. Antes de ejecutar llamadas reales, solicite la base autorizada del entorno y cargue X-API-Key solo desde variables de entorno del backend. Nunca incluya claves en frontend, prompt, URL, logs o repositorio.
3. Cree transacciones con POST /v1/face-auth/transactions e Idempotency-Key estable por operación lógica. Persista transaction_id y external_id. Envíe a la persona a links.webview_url devuelta; no construya la URL de la jornada.
4. Consulte GET /v1/face-auth/transactions/{transaction_id}. Trate status, decision y decision_reason juntos. pending y pasos intermedios siguen pendientes; manual_review_pending espera revisión; approved requiere decision=approved; rejected requiere tratar el rechazo; expired termina o reinicia según la política. Expirar no implica decision=rejected.
5. Preserve los bytes exactos del cuerpo bruto en el receptor de webhooks. Valide X-Face-Auth-Timestamp y X-Face-Auth-Signature (sha256=<hex>) mediante HMAC SHA-256 de timestamp + '.' + raw_body, comparación en tiempo constante y ventana temporal. Solo después interprete JSON y valide el sobre.
6. Deduplicate event_id en almacenamiento duradero, compruebe el identificador del header con el cuerpo y haga idempotentes los efectos de negocio. Reconcilie eventos fuera de orden y entregas perdidas consultando la transacción actual; un evento antiguo no debe degradar una decisión nueva.
7. Entregue configuración, manejo de errores, pruebas con fixtures sintéticas e instrucciones de ejecución. Cubra aprobación, rechazo, pending, expiración, revisión manual, duplicidad, cuerpo alterado, Unicode y timestamp vencido. No ejecute llamadas reales ni procese datos personales sin autorización explícita.
8. cURL cubre solo HTTP. Si se prefiere cURL, use un lenguaje backend soportado para el receptor/HMAC y explique la elección. El MCP siguiente solo consulta documentación pública: no crea transacciones, no firma ni cobra.

Context: https://contrato.cloud/downloads/ai/es/context.md
OpenAPI: https://contrato.cloud/downloads/kyc/openapi.json
Index: https://contrato.cloud/llms.txt
MCP (Streamable HTTP): https://contrato.cloud/api/mcp
MCP prompt: integrate_kyc (locale=es, language=curl)
MCP tools: search_docs, read_doc, get_example

```

### JavaScript / Node.js

```text
Implemente una integración backend de KYC de Contrato.Cloud en JavaScript / Node.js, utilizando solo la documentación pública indicada abajo.
1. Lea el contexto y OpenAPI antes de escribir código. Cite las páginas utilizadas; pida aclaraciones cuando un campo o comportamiento no esté documentado. No invente una API de firmas SIGN, rutas Admin o endpoints privados del Portal.
2. Use https://api.example.invalid como base sintética. Antes de ejecutar llamadas reales, solicite la base autorizada del entorno y cargue X-API-Key solo desde variables de entorno del backend. Nunca incluya claves en frontend, prompt, URL, logs o repositorio.
3. Cree transacciones con POST /v1/face-auth/transactions e Idempotency-Key estable por operación lógica. Persista transaction_id y external_id. Envíe a la persona a links.webview_url devuelta; no construya la URL de la jornada.
4. Consulte GET /v1/face-auth/transactions/{transaction_id}. Trate status, decision y decision_reason juntos. pending y pasos intermedios siguen pendientes; manual_review_pending espera revisión; approved requiere decision=approved; rejected requiere tratar el rechazo; expired termina o reinicia según la política. Expirar no implica decision=rejected.
5. Preserve los bytes exactos del cuerpo bruto en el receptor de webhooks. Valide X-Face-Auth-Timestamp y X-Face-Auth-Signature (sha256=<hex>) mediante HMAC SHA-256 de timestamp + '.' + raw_body, comparación en tiempo constante y ventana temporal. Solo después interprete JSON y valide el sobre.
6. Deduplicate event_id en almacenamiento duradero, compruebe el identificador del header con el cuerpo y haga idempotentes los efectos de negocio. Reconcilie eventos fuera de orden y entregas perdidas consultando la transacción actual; un evento antiguo no debe degradar una decisión nueva.
7. Entregue configuración, manejo de errores, pruebas con fixtures sintéticas e instrucciones de ejecución. Cubra aprobación, rechazo, pending, expiración, revisión manual, duplicidad, cuerpo alterado, Unicode y timestamp vencido. No ejecute llamadas reales ni procese datos personales sin autorización explícita.
8. cURL cubre solo HTTP. Si se prefiere cURL, use un lenguaje backend soportado para el receptor/HMAC y explique la elección. El MCP siguiente solo consulta documentación pública: no crea transacciones, no firma ni cobra.

Context: https://contrato.cloud/downloads/ai/es/context.md
OpenAPI: https://contrato.cloud/downloads/kyc/openapi.json
Index: https://contrato.cloud/llms.txt
MCP (Streamable HTTP): https://contrato.cloud/api/mcp
MCP prompt: integrate_kyc (locale=es, language=javascript)
MCP tools: search_docs, read_doc, get_example

```

### TypeScript / Node.js

```text
Implemente una integración backend de KYC de Contrato.Cloud en TypeScript / Node.js, utilizando solo la documentación pública indicada abajo.
1. Lea el contexto y OpenAPI antes de escribir código. Cite las páginas utilizadas; pida aclaraciones cuando un campo o comportamiento no esté documentado. No invente una API de firmas SIGN, rutas Admin o endpoints privados del Portal.
2. Use https://api.example.invalid como base sintética. Antes de ejecutar llamadas reales, solicite la base autorizada del entorno y cargue X-API-Key solo desde variables de entorno del backend. Nunca incluya claves en frontend, prompt, URL, logs o repositorio.
3. Cree transacciones con POST /v1/face-auth/transactions e Idempotency-Key estable por operación lógica. Persista transaction_id y external_id. Envíe a la persona a links.webview_url devuelta; no construya la URL de la jornada.
4. Consulte GET /v1/face-auth/transactions/{transaction_id}. Trate status, decision y decision_reason juntos. pending y pasos intermedios siguen pendientes; manual_review_pending espera revisión; approved requiere decision=approved; rejected requiere tratar el rechazo; expired termina o reinicia según la política. Expirar no implica decision=rejected.
5. Preserve los bytes exactos del cuerpo bruto en el receptor de webhooks. Valide X-Face-Auth-Timestamp y X-Face-Auth-Signature (sha256=<hex>) mediante HMAC SHA-256 de timestamp + '.' + raw_body, comparación en tiempo constante y ventana temporal. Solo después interprete JSON y valide el sobre.
6. Deduplicate event_id en almacenamiento duradero, compruebe el identificador del header con el cuerpo y haga idempotentes los efectos de negocio. Reconcilie eventos fuera de orden y entregas perdidas consultando la transacción actual; un evento antiguo no debe degradar una decisión nueva.
7. Entregue configuración, manejo de errores, pruebas con fixtures sintéticas e instrucciones de ejecución. Cubra aprobación, rechazo, pending, expiración, revisión manual, duplicidad, cuerpo alterado, Unicode y timestamp vencido. No ejecute llamadas reales ni procese datos personales sin autorización explícita.
8. cURL cubre solo HTTP. Si se prefiere cURL, use un lenguaje backend soportado para el receptor/HMAC y explique la elección. El MCP siguiente solo consulta documentación pública: no crea transacciones, no firma ni cobra.

Context: https://contrato.cloud/downloads/ai/es/context.md
OpenAPI: https://contrato.cloud/downloads/kyc/openapi.json
Index: https://contrato.cloud/llms.txt
MCP (Streamable HTTP): https://contrato.cloud/api/mcp
MCP prompt: integrate_kyc (locale=es, language=typescript)
MCP tools: search_docs, read_doc, get_example

```

### Python

```text
Implemente una integración backend de KYC de Contrato.Cloud en Python, utilizando solo la documentación pública indicada abajo.
1. Lea el contexto y OpenAPI antes de escribir código. Cite las páginas utilizadas; pida aclaraciones cuando un campo o comportamiento no esté documentado. No invente una API de firmas SIGN, rutas Admin o endpoints privados del Portal.
2. Use https://api.example.invalid como base sintética. Antes de ejecutar llamadas reales, solicite la base autorizada del entorno y cargue X-API-Key solo desde variables de entorno del backend. Nunca incluya claves en frontend, prompt, URL, logs o repositorio.
3. Cree transacciones con POST /v1/face-auth/transactions e Idempotency-Key estable por operación lógica. Persista transaction_id y external_id. Envíe a la persona a links.webview_url devuelta; no construya la URL de la jornada.
4. Consulte GET /v1/face-auth/transactions/{transaction_id}. Trate status, decision y decision_reason juntos. pending y pasos intermedios siguen pendientes; manual_review_pending espera revisión; approved requiere decision=approved; rejected requiere tratar el rechazo; expired termina o reinicia según la política. Expirar no implica decision=rejected.
5. Preserve los bytes exactos del cuerpo bruto en el receptor de webhooks. Valide X-Face-Auth-Timestamp y X-Face-Auth-Signature (sha256=<hex>) mediante HMAC SHA-256 de timestamp + '.' + raw_body, comparación en tiempo constante y ventana temporal. Solo después interprete JSON y valide el sobre.
6. Deduplicate event_id en almacenamiento duradero, compruebe el identificador del header con el cuerpo y haga idempotentes los efectos de negocio. Reconcilie eventos fuera de orden y entregas perdidas consultando la transacción actual; un evento antiguo no debe degradar una decisión nueva.
7. Entregue configuración, manejo de errores, pruebas con fixtures sintéticas e instrucciones de ejecución. Cubra aprobación, rechazo, pending, expiración, revisión manual, duplicidad, cuerpo alterado, Unicode y timestamp vencido. No ejecute llamadas reales ni procese datos personales sin autorización explícita.
8. cURL cubre solo HTTP. Si se prefiere cURL, use un lenguaje backend soportado para el receptor/HMAC y explique la elección. El MCP siguiente solo consulta documentación pública: no crea transacciones, no firma ni cobra.

Context: https://contrato.cloud/downloads/ai/es/context.md
OpenAPI: https://contrato.cloud/downloads/kyc/openapi.json
Index: https://contrato.cloud/llms.txt
MCP (Streamable HTTP): https://contrato.cloud/api/mcp
MCP prompt: integrate_kyc (locale=es, language=python)
MCP tools: search_docs, read_doc, get_example

```

### PHP

```text
Implemente una integración backend de KYC de Contrato.Cloud en PHP, utilizando solo la documentación pública indicada abajo.
1. Lea el contexto y OpenAPI antes de escribir código. Cite las páginas utilizadas; pida aclaraciones cuando un campo o comportamiento no esté documentado. No invente una API de firmas SIGN, rutas Admin o endpoints privados del Portal.
2. Use https://api.example.invalid como base sintética. Antes de ejecutar llamadas reales, solicite la base autorizada del entorno y cargue X-API-Key solo desde variables de entorno del backend. Nunca incluya claves en frontend, prompt, URL, logs o repositorio.
3. Cree transacciones con POST /v1/face-auth/transactions e Idempotency-Key estable por operación lógica. Persista transaction_id y external_id. Envíe a la persona a links.webview_url devuelta; no construya la URL de la jornada.
4. Consulte GET /v1/face-auth/transactions/{transaction_id}. Trate status, decision y decision_reason juntos. pending y pasos intermedios siguen pendientes; manual_review_pending espera revisión; approved requiere decision=approved; rejected requiere tratar el rechazo; expired termina o reinicia según la política. Expirar no implica decision=rejected.
5. Preserve los bytes exactos del cuerpo bruto en el receptor de webhooks. Valide X-Face-Auth-Timestamp y X-Face-Auth-Signature (sha256=<hex>) mediante HMAC SHA-256 de timestamp + '.' + raw_body, comparación en tiempo constante y ventana temporal. Solo después interprete JSON y valide el sobre.
6. Deduplicate event_id en almacenamiento duradero, compruebe el identificador del header con el cuerpo y haga idempotentes los efectos de negocio. Reconcilie eventos fuera de orden y entregas perdidas consultando la transacción actual; un evento antiguo no debe degradar una decisión nueva.
7. Entregue configuración, manejo de errores, pruebas con fixtures sintéticas e instrucciones de ejecución. Cubra aprobación, rechazo, pending, expiración, revisión manual, duplicidad, cuerpo alterado, Unicode y timestamp vencido. No ejecute llamadas reales ni procese datos personales sin autorización explícita.
8. cURL cubre solo HTTP. Si se prefiere cURL, use un lenguaje backend soportado para el receptor/HMAC y explique la elección. El MCP siguiente solo consulta documentación pública: no crea transacciones, no firma ni cobra.

Context: https://contrato.cloud/downloads/ai/es/context.md
OpenAPI: https://contrato.cloud/downloads/kyc/openapi.json
Index: https://contrato.cloud/llms.txt
MCP (Streamable HTTP): https://contrato.cloud/api/mcp
MCP prompt: integrate_kyc (locale=es, language=php)
MCP tools: search_docs, read_doc, get_example

```

### C# / .NET

```text
Implemente una integración backend de KYC de Contrato.Cloud en C# / .NET, utilizando solo la documentación pública indicada abajo.
1. Lea el contexto y OpenAPI antes de escribir código. Cite las páginas utilizadas; pida aclaraciones cuando un campo o comportamiento no esté documentado. No invente una API de firmas SIGN, rutas Admin o endpoints privados del Portal.
2. Use https://api.example.invalid como base sintética. Antes de ejecutar llamadas reales, solicite la base autorizada del entorno y cargue X-API-Key solo desde variables de entorno del backend. Nunca incluya claves en frontend, prompt, URL, logs o repositorio.
3. Cree transacciones con POST /v1/face-auth/transactions e Idempotency-Key estable por operación lógica. Persista transaction_id y external_id. Envíe a la persona a links.webview_url devuelta; no construya la URL de la jornada.
4. Consulte GET /v1/face-auth/transactions/{transaction_id}. Trate status, decision y decision_reason juntos. pending y pasos intermedios siguen pendientes; manual_review_pending espera revisión; approved requiere decision=approved; rejected requiere tratar el rechazo; expired termina o reinicia según la política. Expirar no implica decision=rejected.
5. Preserve los bytes exactos del cuerpo bruto en el receptor de webhooks. Valide X-Face-Auth-Timestamp y X-Face-Auth-Signature (sha256=<hex>) mediante HMAC SHA-256 de timestamp + '.' + raw_body, comparación en tiempo constante y ventana temporal. Solo después interprete JSON y valide el sobre.
6. Deduplicate event_id en almacenamiento duradero, compruebe el identificador del header con el cuerpo y haga idempotentes los efectos de negocio. Reconcilie eventos fuera de orden y entregas perdidas consultando la transacción actual; un evento antiguo no debe degradar una decisión nueva.
7. Entregue configuración, manejo de errores, pruebas con fixtures sintéticas e instrucciones de ejecución. Cubra aprobación, rechazo, pending, expiración, revisión manual, duplicidad, cuerpo alterado, Unicode y timestamp vencido. No ejecute llamadas reales ni procese datos personales sin autorización explícita.
8. cURL cubre solo HTTP. Si se prefiere cURL, use un lenguaje backend soportado para el receptor/HMAC y explique la elección. El MCP siguiente solo consulta documentación pública: no crea transacciones, no firma ni cobra.

Context: https://contrato.cloud/downloads/ai/es/context.md
OpenAPI: https://contrato.cloud/downloads/kyc/openapi.json
Index: https://contrato.cloud/llms.txt
MCP (Streamable HTTP): https://contrato.cloud/api/mcp
MCP prompt: integrate_kyc (locale=es, language=csharp)
MCP tools: search_docs, read_doc, get_example

```

### Java

```text
Implemente una integración backend de KYC de Contrato.Cloud en Java, utilizando solo la documentación pública indicada abajo.
1. Lea el contexto y OpenAPI antes de escribir código. Cite las páginas utilizadas; pida aclaraciones cuando un campo o comportamiento no esté documentado. No invente una API de firmas SIGN, rutas Admin o endpoints privados del Portal.
2. Use https://api.example.invalid como base sintética. Antes de ejecutar llamadas reales, solicite la base autorizada del entorno y cargue X-API-Key solo desde variables de entorno del backend. Nunca incluya claves en frontend, prompt, URL, logs o repositorio.
3. Cree transacciones con POST /v1/face-auth/transactions e Idempotency-Key estable por operación lógica. Persista transaction_id y external_id. Envíe a la persona a links.webview_url devuelta; no construya la URL de la jornada.
4. Consulte GET /v1/face-auth/transactions/{transaction_id}. Trate status, decision y decision_reason juntos. pending y pasos intermedios siguen pendientes; manual_review_pending espera revisión; approved requiere decision=approved; rejected requiere tratar el rechazo; expired termina o reinicia según la política. Expirar no implica decision=rejected.
5. Preserve los bytes exactos del cuerpo bruto en el receptor de webhooks. Valide X-Face-Auth-Timestamp y X-Face-Auth-Signature (sha256=<hex>) mediante HMAC SHA-256 de timestamp + '.' + raw_body, comparación en tiempo constante y ventana temporal. Solo después interprete JSON y valide el sobre.
6. Deduplicate event_id en almacenamiento duradero, compruebe el identificador del header con el cuerpo y haga idempotentes los efectos de negocio. Reconcilie eventos fuera de orden y entregas perdidas consultando la transacción actual; un evento antiguo no debe degradar una decisión nueva.
7. Entregue configuración, manejo de errores, pruebas con fixtures sintéticas e instrucciones de ejecución. Cubra aprobación, rechazo, pending, expiración, revisión manual, duplicidad, cuerpo alterado, Unicode y timestamp vencido. No ejecute llamadas reales ni procese datos personales sin autorización explícita.
8. cURL cubre solo HTTP. Si se prefiere cURL, use un lenguaje backend soportado para el receptor/HMAC y explique la elección. El MCP siguiente solo consulta documentación pública: no crea transacciones, no firma ni cobra.

Context: https://contrato.cloud/downloads/ai/es/context.md
OpenAPI: https://contrato.cloud/downloads/kyc/openapi.json
Index: https://contrato.cloud/llms.txt
MCP (Streamable HTTP): https://contrato.cloud/api/mcp
MCP prompt: integrate_kyc (locale=es, language=java)
MCP tools: search_docs, read_doc, get_example

```

### Go

```text
Implemente una integración backend de KYC de Contrato.Cloud en Go, utilizando solo la documentación pública indicada abajo.
1. Lea el contexto y OpenAPI antes de escribir código. Cite las páginas utilizadas; pida aclaraciones cuando un campo o comportamiento no esté documentado. No invente una API de firmas SIGN, rutas Admin o endpoints privados del Portal.
2. Use https://api.example.invalid como base sintética. Antes de ejecutar llamadas reales, solicite la base autorizada del entorno y cargue X-API-Key solo desde variables de entorno del backend. Nunca incluya claves en frontend, prompt, URL, logs o repositorio.
3. Cree transacciones con POST /v1/face-auth/transactions e Idempotency-Key estable por operación lógica. Persista transaction_id y external_id. Envíe a la persona a links.webview_url devuelta; no construya la URL de la jornada.
4. Consulte GET /v1/face-auth/transactions/{transaction_id}. Trate status, decision y decision_reason juntos. pending y pasos intermedios siguen pendientes; manual_review_pending espera revisión; approved requiere decision=approved; rejected requiere tratar el rechazo; expired termina o reinicia según la política. Expirar no implica decision=rejected.
5. Preserve los bytes exactos del cuerpo bruto en el receptor de webhooks. Valide X-Face-Auth-Timestamp y X-Face-Auth-Signature (sha256=<hex>) mediante HMAC SHA-256 de timestamp + '.' + raw_body, comparación en tiempo constante y ventana temporal. Solo después interprete JSON y valide el sobre.
6. Deduplicate event_id en almacenamiento duradero, compruebe el identificador del header con el cuerpo y haga idempotentes los efectos de negocio. Reconcilie eventos fuera de orden y entregas perdidas consultando la transacción actual; un evento antiguo no debe degradar una decisión nueva.
7. Entregue configuración, manejo de errores, pruebas con fixtures sintéticas e instrucciones de ejecución. Cubra aprobación, rechazo, pending, expiración, revisión manual, duplicidad, cuerpo alterado, Unicode y timestamp vencido. No ejecute llamadas reales ni procese datos personales sin autorización explícita.
8. cURL cubre solo HTTP. Si se prefiere cURL, use un lenguaje backend soportado para el receptor/HMAC y explique la elección. El MCP siguiente solo consulta documentación pública: no crea transacciones, no firma ni cobra.

Context: https://contrato.cloud/downloads/ai/es/context.md
OpenAPI: https://contrato.cloud/downloads/kyc/openapi.json
Index: https://contrato.cloud/llms.txt
MCP (Streamable HTTP): https://contrato.cloud/api/mcp
MCP prompt: integrate_kyc (locale=es, language=go)
MCP tools: search_docs, read_doc, get_example

```

## Valide antes de ejecutar

Revise el código generado, configure la base autorizada en su backend y pruebe idempotencia, HMAC, reconciliación, expiración y revisión manual. Consulte el [checklist de producción](/es/desarrolladores/kyc/paso-a-produccion). La documentación de IA y MCP no reciben claves KYC ni datos de sus clientes.


---

# Autenticación y protección de las claves de API

Source: https://contrato.cloud/es/desarrolladores/kyc/autenticacion

## Autenticación entre servidores

La API B2B utiliza el header `X-API-Key`. La clave determina el Tenant y el entorno; enviar el identificador de otra empresa no cambia ese alcance. Las credenciales de API son distintas del acceso al Portal y de los tokens de sesión del navegador.

```http
X-API-Key: REPLACE_ON_YOUR_SERVER
```

## Proteja el secreto

- Guarde la clave en un gestor de secretos o en la configuración protegida del servidor.
- Nunca la incluya en JavaScript público, URLs, aplicaciones distribuidas, repositorios o consultas de soporte.
- Registre únicamente datos operativos necesarios. Oculte headers y datos personales en los logs.
- Separe credenciales y configuración por entorno. Conceda solo los permisos necesarios.

Ante una posible exposición, deje de utilizar la clave comprometida, coordine su revocación y sustitución con el responsable del entorno y confirme que los consumidores utilizan la nueva credencial. La rotación no debe requerir cambios en la aplicación de la persona verificada.

## Diagnóstico del acceso

`401` puede indicar una clave ausente, inválida, revocada o incompatible con el entorno. Según la operación, `403` también puede representar un entorno deshabilitado, permisos insuficientes o una IP no autorizada. Evite reintentos infinitos.

Las descargas requieren autorización propia; acceder a una transacción no habilita automáticamente sus archivos. Consulte [documentos y evidencias](/es/desarrolladores/kyc/documentos-y-evidencias) y los códigos de la [referencia](/es/desarrolladores/kyc/referencia).


---

# Entornos y configuración de la integración KYC

Source: https://contrato.cloud/es/desarrolladores/kyc/entornos

## Cada entorno tiene su contexto

Configure como un conjunto la base de API, la clave y el receptor de webhooks. Una transacción pertenece al Tenant y entorno resueltos por la credencial. Un identificador válido en otro entorno no concede acceso y puede devolver `404`.

```text
KYC_API_BASE_URL=https://api.example.invalid
KYC_API_KEY=REPLACE_ON_YOUR_SERVER
KYC_WEBHOOK_SECRET=REPLACE_ON_YOUR_SERVER
```

Estos valores son placeholders. Una URL de ejemplo no demuestra que un servicio esté activo. Confirme las direcciones y los accesos oficiales durante la habilitación de la integración.

## Separe también su sistema

Utilice configuraciones independientes para bases de datos, colas y callbacks. Incluya el entorno en la clave local de asociación de la transacción. Una rutina de prueba nunca debe activar crédito, liberar una cuenta ni modificar contratos reales.

Utilice datos sintéticos en las pruebas de contrato. Cuando la homologación requiera un recorrido real, acuerde finalidad, acceso y tratamiento de datos con los responsables de la operación. No envíe documentos personales al simulador público: utiliza únicamente escenarios fijos en el navegador.

## Antes de cambiar de entorno

Confirme credenciales, permisos de descarga, configuración de callback, secreto HMAC y reglas de continuidad. Compruebe también las IPs y URLs autorizadas. Repita las pruebas de rechazo, vencimiento y conciliación; una conexión exitosa no sustituye la validación funcional. Consulte la [lista de producción](/es/desarrolladores/kyc/paso-a-produccion).


---

# Cree una transacción de verificación de identidad

Source: https://contrato.cloud/es/desarrolladores/kyc/crear-transaccion

## Defina el proceso que se verificará

Cree la transacción mediante `POST /v1/face-auth/transactions`. Los campos obligatorios son `country_code` y `subject`; dentro de `subject`, indique `identity` con `type`, `number` y `country`. Consulte los países y locales admitidos en la referencia; los idiomas del sitio no equivalen a los valores de la API.

Crear transacción KYC

### cURL

```bash
# Server-side only. Configure KYC_API_BASE_URL with your authorized base (https://api.example.invalid is illustrative), and KYC_API_KEY privately. Never expose credentials in browser code.
set -eu
: "${KYC_API_BASE_URL:?Set authorized base}" "${KYC_API_KEY:?Set API key}"
: "${KYC_IDEMPOTENCY_KEY:?Persist the key for this creation intent}"
curl --fail-with-body --max-time 15 --request POST \
  "${KYC_API_BASE_URL%/}/v1/face-auth/transactions" \
  --header "X-API-Key: ${KYC_API_KEY}" \
  --header "Idempotency-Key: ${KYC_IDEMPOTENCY_KEY}" \
  --header 'Content-Type: application/json' \
  --data-binary '{"country_code":"BR","external_id":"docs-example-0001","locale":"pt-BR","subject":{"identity":{"country":"BR","number":"00000000000","type":"CPF"}}}'
# Persist transaction_id and business correlation; use the returned links.webview_url. Read status, decision and decision_reason together. Reuse the SAME idempotency key AND original payload only for the same creation intent; never silently bypass a 409 conflict.
```

### JavaScript / Node.js

```javascript
import process from "node:process";
// Server-side only. Configure KYC_API_BASE_URL with your authorized base (https://api.example.invalid is illustrative), and KYC_API_KEY privately. Never expose credentials in browser code.
function required(name) {
  const value = process.env[name];
  if (!value) throw new Error("Missing configuration: " + name);
  return value;
}
const base = required("KYC_API_BASE_URL").replace(/\/$/, "");
const response = await fetch(base + "/v1/face-auth/transactions", {
  method: "POST",
  headers: { "X-API-Key": required("KYC_API_KEY"), "Content-Type": "application/json", "Idempotency-Key": required("KYC_IDEMPOTENCY_KEY") },
  body: "{\"country_code\":\"BR\",\"external_id\":\"docs-example-0001\",\"locale\":\"pt-BR\",\"subject\":{\"identity\":{\"country\":\"BR\",\"number\":\"00000000000\",\"type\":\"CPF\"}}}",
  signal: AbortSignal.timeout(15_000),
});
if (response.status !== 201) throw new Error("KYC HTTP " + response.status);
const transaction = await response.json();
console.log("HTTP " + response.status);
// Persist transaction_id and business correlation; use the returned links.webview_url. Read status, decision and decision_reason together. Reuse the SAME idempotency key AND original payload only for the same creation intent; never silently bypass a 409 conflict.
```

### TypeScript / Node.js

```typescript
import process from "node:process";
// Server-side only. Configure KYC_API_BASE_URL with your authorized base (https://api.example.invalid is illustrative), and KYC_API_KEY privately. Never expose credentials in browser code.
function required(name: string): string {
  const value = process.env[name];
  if (!value) throw new Error("Missing configuration: " + name);
  return value;
}
const base = required("KYC_API_BASE_URL").replace(/\/$/, "");
const response = await fetch(base + "/v1/face-auth/transactions", {
  method: "POST",
  headers: { "X-API-Key": required("KYC_API_KEY"), "Content-Type": "application/json", "Idempotency-Key": required("KYC_IDEMPOTENCY_KEY") },
  body: "{\"country_code\":\"BR\",\"external_id\":\"docs-example-0001\",\"locale\":\"pt-BR\",\"subject\":{\"identity\":{\"country\":\"BR\",\"number\":\"00000000000\",\"type\":\"CPF\"}}}",
  signal: AbortSignal.timeout(15_000),
});
if (response.status !== 201) throw new Error("KYC HTTP " + response.status);
const transaction: unknown = await response.json();
// Validate the response against the curated OpenAPI before using it.
console.log("HTTP " + response.status);
// Persist transaction_id and business correlation; use the returned links.webview_url. Read status, decision and decision_reason together. Reuse the SAME idempotency key AND original payload only for the same creation intent; never silently bypass a 409 conflict.
```

### Python

```python
import json
import os
from urllib.parse import quote
from urllib.request import Request, urlopen
# Server-side only. Configure KYC_API_BASE_URL with your authorized base (https://api.example.invalid is illustrative), and KYC_API_KEY privately. Never expose credentials in browser code.
base = os.environ["KYC_API_BASE_URL"].rstrip("/")
request = Request(base + "/v1/face-auth/transactions",
    method="POST",
    headers={"X-API-Key": os.environ["KYC_API_KEY"], "Content-Type": "application/json",
             "Idempotency-Key": os.environ["KYC_IDEMPOTENCY_KEY"]},
    data="{\"country_code\":\"BR\",\"external_id\":\"docs-example-0001\",\"locale\":\"pt-BR\",\"subject\":{\"identity\":{\"country\":\"BR\",\"number\":\"00000000000\",\"type\":\"CPF\"}}}".encode("utf-8"),
)
with urlopen(request, timeout=15) as response:
    if response.status != 201:
        raise RuntimeError("KYC HTTP " + str(response.status))
    transaction = json.load(response)
    print("HTTP " + str(response.status))
# Persist transaction_id and business correlation; use the returned links.webview_url. Read status, decision and decision_reason together. Reuse the SAME idempotency key AND original payload only for the same creation intent; never silently bypass a 409 conflict.
```

### PHP

```php
<?php
// PHP 8+, standard HTTP streams. Server-side only. Configure KYC_API_BASE_URL with your authorized base (https://api.example.invalid is illustrative), and KYC_API_KEY privately. Never expose credentials in browser code.
function required(string $name): string {
    $value = getenv($name);
    if ($value === false || $value === '') throw new RuntimeException("Missing configuration: " . $name);
    return $value;
}
$url = rtrim(required('KYC_API_BASE_URL'), '/') . '/v1/face-auth/transactions';
$headers = ['X-API-Key: ' . required('KYC_API_KEY'), 'Content-Type: application/json', 'Idempotency-Key: ' . required('KYC_IDEMPOTENCY_KEY')];
$context = stream_context_create(['http' => ['method' => 'POST', 'header' => implode("\r\n", $headers),
    'timeout' => 15, 'ignore_errors' => true, 'content' => '{"country_code":"BR","external_id":"docs-example-0001","locale":"pt-BR","subject":{"identity":{"country":"BR","number":"00000000000","type":"CPF"}}}']]);
$body = file_get_contents($url, false, $context);
if ($body === false) throw new RuntimeException('KYC transport failure');
if (!preg_match('/^HTTP\/\S+ ([0-9]{3})/', $http_response_header[0] ?? '', $matches)) throw new RuntimeException('Invalid HTTP response');
$status = (int) $matches[1];
if ($status !== 201) throw new RuntimeException('KYC HTTP ' . $status);
$transaction = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
echo 'HTTP ' . $status . PHP_EOL;
// Persist transaction_id and business correlation; use the returned links.webview_url. Read status, decision and decision_reason together. Reuse the SAME idempotency key AND original payload only for the same creation intent; never silently bypass a 409 conflict.
```

### C# / .NET

```csharp
using System;
using System.Net.Http;
using System.Text;
using System.Text.Json;
// .NET 8+. Server-side only. Configure KYC_API_BASE_URL with your authorized base (https://api.example.invalid is illustrative), and KYC_API_KEY privately. Never expose credentials in browser code.
static string Required(string name) => Environment.GetEnvironmentVariable(name) is { Length: > 0 } value ? value : throw new Exception("Missing configuration: " + name);
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(15) };
using var request = new HttpRequestMessage(HttpMethod.Post, Required("KYC_API_BASE_URL").TrimEnd('/') + "/v1/face-auth/transactions");
request.Headers.Add("X-API-Key", Required("KYC_API_KEY"));
request.Headers.Add("Idempotency-Key", Required("KYC_IDEMPOTENCY_KEY"));
request.Content = new StringContent("{\"country_code\":\"BR\",\"external_id\":\"docs-example-0001\",\"locale\":\"pt-BR\",\"subject\":{\"identity\":{\"country\":\"BR\",\"number\":\"00000000000\",\"type\":\"CPF\"}}}", Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
if ((int)response.StatusCode != 201) throw new Exception("KYC HTTP " + (int)response.StatusCode);
using var transaction = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
Console.WriteLine("HTTP " + (int)response.StatusCode);
// Persist transaction_id and business correlation; use the returned links.webview_url. Read status, decision and decision_reason together. Reuse the SAME idempotency key AND original payload only for the same creation intent; never silently bypass a 409 conflict.
```

### Java

```java
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.*;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
// Java 17+. Server-side only. Configure KYC_API_BASE_URL with your authorized base (https://api.example.invalid is illustrative), and KYC_API_KEY privately. Never expose credentials in browser code.
class Example {
  static String required(String name) {
    String value = System.getenv(name);
    if (value == null || value.isEmpty()) throw new IllegalStateException("Missing configuration: " + name);
    return value;
  }
  public static void main(String[] args) throws Exception {
    String base = required("KYC_API_BASE_URL").replaceAll("/+$", "");
    String url = base + "/v1/face-auth/transactions";
    var builder = HttpRequest.newBuilder(URI.create(url)).timeout(Duration.ofSeconds(15))
        .header("X-API-Key", required("KYC_API_KEY"));
    builder.header("Idempotency-Key", required("KYC_IDEMPOTENCY_KEY"))
        .header("Content-Type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString("{\"country_code\":\"BR\",\"external_id\":\"docs-example-0001\",\"locale\":\"pt-BR\",\"subject\":{\"identity\":{\"country\":\"BR\",\"number\":\"00000000000\",\"type\":\"CPF\"}}}", StandardCharsets.UTF_8));
    var response = HttpClient.newHttpClient().send(builder.build(), HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
    if (response.statusCode() != 201) throw new IllegalStateException("KYC HTTP " + response.statusCode());
    System.out.println("HTTP " + response.statusCode());
    // Parse response.body() with your application's JSON parser and validate against OpenAPI.
    // Persist transaction_id and business correlation; use the returned links.webview_url. Read status, decision and decision_reason together. Reuse the SAME idempotency key AND original payload only for the same creation intent; never silently bypass a 409 conflict.
  }
}
```

### Go

```go
package main
import (
  "encoding/json"
  "fmt"
  "net/http"
  "net/url"
  "os"
  "strings"
  "time"
)
// Server-side only. Configure KYC_API_BASE_URL with your authorized base (https://api.example.invalid is illustrative), and KYC_API_KEY privately. Never expose credentials in browser code.
func required(name string) string {
  value := os.Getenv(name); if value == "" { panic("Missing configuration: " + name) }; return value
}
func main() {
  _ = url.PathEscape // Used by the GET variant.
  endpoint := strings.TrimRight(required("KYC_API_BASE_URL"), "/") + "/v1/face-auth/transactions"
  request, err := http.NewRequest("POST", endpoint, strings.NewReader(`{"country_code":"BR","external_id":"docs-example-0001","locale":"pt-BR","subject":{"identity":{"country":"BR","number":"00000000000","type":"CPF"}}}`)); if err != nil { panic(err) }
  request.Header.Set("X-API-Key", required("KYC_API_KEY"))
  request.Header.Set("Content-Type", "application/json")
  request.Header.Set("Idempotency-Key", required("KYC_IDEMPOTENCY_KEY"))
  response, err := (&http.Client{Timeout: 15*time.Second}).Do(request); if err != nil { panic(err) }; defer response.Body.Close()
  if response.StatusCode != 201 { panic(fmt.Sprintf("KYC HTTP %d", response.StatusCode)) }
  var transaction map[string]any
  if err := json.NewDecoder(response.Body).Decode(&transaction); err != nil { panic(err) }
  fmt.Printf("HTTP %d\n", response.StatusCode)
  // Persist transaction_id and business correlation; use the returned links.webview_url. Read status, decision and decision_reason together. Reuse the SAME idempotency key AND original payload only for the same creation intent; never silently bypass a 409 conflict.
}
```

## Correlacione sin duplicar datos

Utilice `external_id` para relacionar el proceso en su sistema. `subject.tenant_subject_id` identifica a la persona en su base. Estos identificadores tienen funciones distintas y no sustituyen el `transaction_id` devuelto por la API.

`source` describe el sistema, la integración y el canal de origen. `metadata` admite contexto de negocio, con un límite de 8 KB; evite datos sensibles e información innecesaria. Puede configurar el vencimiento mediante `expires_in_minutes`, dentro de los límites del contrato; si se omite, se aplica el valor del Tenant.

## Trate la respuesta

La respuesta documentada es `201`, incluso si la creación se recupera por idempotencia. Guarde el identificador antes de derivar a la persona. Si pierde la respuesta después del envío, repita la creación con la misma clave y el mismo payload; no genere automáticamente otra clave.

`callback_url`, si se proporciona, define el webhook de la transacción; de lo contrario se utiliza la configuración del Tenant. Consulte [idempotencia](/es/desarrolladores/kyc/idempotencia) y [webhooks](/es/desarrolladores/kyc/webhooks-y-eventos) antes de producción.


---

# Derive a la persona al recorrido alojado

Source: https://contrato.cloud/es/desarrolladores/kyc/recorrido-alojado

## Utilice la URL devuelta por la API

Después de crear una transacción, utilice un enlace de recorrido devuelto en la respuesta, como `links.webview_url`. No construya URLs concatenando `transaction_id`, un documento de identidad o un código de cliente. Las URLs de documentación son sintéticas y no identifican a una persona real.

La persona realiza la verificación en un navegador compatible. Su aplicación no necesita recibir ni guardar la clave de API para abrir este flujo. La clave permanece en el servidor que creó la transacción.

## Trate el enlace como un acceso sensible

Compártalo únicamente con la persona y por los canales previstos en su proceso. No registre la URL completa en herramientas públicas de analytics, logs sin control ni mensajes de soporte abiertos. Proteja también los enlaces cortos: siguen permitiendo el acceso al recorrido.

Las etapas exigidas dependen de la configuración aplicable a la transacción. No sustituya una etapa obligatoria por una pantalla local ni marque el recorrido como finalizado porque la persona cerró el navegador.

## Acompañe sin suposiciones

Abrir un enlace no equivale a una aprobación. Consulte la transacción desde su backend y procese eventos autenticados. Una interrupción puede dejar el flujo pendiente; el vencimiento requiere una decisión en su proceso, sin reutilizar resultados de otra transacción.

Consulte [resultados y estados](/es/desarrolladores/kyc/resultados-y-estados) y [webhooks](/es/desarrolladores/kyc/webhooks-y-eventos) para recibir actualizaciones independientemente de la pantalla de la persona.


---

# Interprete resultados, decisiones y estados KYC

Source: https://contrato.cloud/es/desarrolladores/kyc/resultados-y-estados

## Lea la transacción en conjunto

`status` describe el avance del recorrido. `decision` expresa `pending`, `approved` o `rejected`. Cuando está presente, `decision_reason` añade el motivo documentado. Consulte la referencia para el objeto completo y los enums vigentes; no deduzca una decisión a partir de una sola etapa.

| Escenario | Tratamiento en su sistema |
| --- | --- |
| `created` y etapas intermedias | Mantenga el proceso pendiente |
| `manual_review_pending` | Espere la revisión; no libere como aprobado |
| `approved` con `decision=approved` | Aplique sus reglas para continuar |
| `rejected` con `decision=rejected` | Trate el rechazo según su procedimiento |
| `expired` | Cierre o reinicie según su política |

Estados como `liveness_passed` o `document_passed` describen una etapa finalizada y no sustituyen la decisión final. Una conexión interrumpida tampoco demuestra rechazo ni aprobación.

## Sincronice su decisión de negocio

Guarde la relación entre su proceso y la transacción. Al recibir un evento, valide el origen, reconozca duplicados y consulte el estado actual antes de ejecutar efectos irreversibles. Un evento antiguo no debe sobrescribir una decisión posterior por su orden de llegada.

La aprobación KYC no determina la aprobación de crédito, la ausencia de fraude en una compra ni las facultades de representación. Esas evaluaciones siguen a cargo de su operación. Consulte [reentrega y conciliación](/es/desarrolladores/kyc/reentrega-y-conciliacion) y pruebe estos escenarios en el [simulador](/es/desarrolladores/kyc/simulador).


---

# Idempotencia: repita la creación sin duplicar la transacción

Source: https://contrato.cloud/es/desarrolladores/kyc/idempotencia

## Una clave para la misma intención

Envíe `Idempotency-Key` al crear una transacción. El header es opcional en el contrato, pero se recomienda para tolerar una conexión perdida después de que el servidor recibe la solicitud. Genere y guarde la clave antes del primer POST; reutilícela solo para la misma creación y el mismo payload.

```http
Idempotency-Key: onboarding-demo-0001
```

## Perder la respuesta no significa perder la creación

Si su servidor envía una solicitud y recibe un timeout, la transacción puede existir. Repetir con otra clave puede crear un segundo recorrido. Repetir el mismo POST con la misma clave y payload permite recuperar la creación idempotente; la respuesta documentada sigue siendo `201`.

Utilice `external_id` para correlación de negocio, sin considerarlo un sustituto automático del header. Conserve la relación entre clave, intento y transacción en su sistema.

## Trate el conflicto explícitamente

La misma clave con un payload diferente devuelve `409` con código `idempotency_conflict`. No evite ese resultado cambiando la clave silenciosamente. Investigue si el cliente modificó los datos o si su aplicación reconstruyó la solicitud de forma inconsistente.

Una intención nueva recibe una clave nueva. Un reintento técnico conserva la intención anterior. No suponga un plazo de retención de claves no documentado para su contrato.

La idempotencia de creación no sustituye la deduplicación de [webhooks](/es/desarrolladores/kyc/webhooks-y-eventos). Para eventos, utilice `event_id` y haga también idempotentes los efectos de negocio.


---

# Reciba webhooks y acompañe los eventos KYC

Source: https://contrato.cloud/es/desarrolladores/kyc/webhooks-y-eventos

## Configure un receptor en su backend

Indique `callback_url` al crear la transacción para definir su destino. Si se omite, se utiliza el webhook configurado en el Tenant. El receptor debe admitir el formato documentado y conservar los bytes recibidos antes de interpretar JSON.

El payload incluye `api_version`, `event_id`, `event_type`, `created_at` y `transaction`. Los headers `X-Face-Auth-Event-Id`, `X-Face-Auth-Event-Type`, `X-Face-Auth-Timestamp` y `X-Face-Auth-Signature` identifican y autentican la entrega. Consulte la [referencia completa](/es/desarrolladores/kyc/referencia) para los enums de eventos.

## Una entrega puede repetirse

Deduplicar por `transaction_id` descartaría eventos diferentes del mismo recorrido. Utilice `event_id` con almacenamiento duradero y protegido frente a concurrencia. Valide la firma antes de confiar en el evento y compruebe la coherencia del identificador entre el envelope y el header.

El receptor puede guardar el evento validado en una cola duradera y procesarlo después. Responda con éxito solo tras aceptar el trabajo de forma persistente; una confirmación no debe ocultar pérdida de datos.

## No dependa del orden de llegada

Un reintento o retraso puede entregar un evento antiguo después de uno más reciente. Consulte la transacción mediante GET para conciliar el estado antes de ejecutar efectos de negocio. Haga esos efectos idempotentes para evitar dos liberaciones ante un reintento.

Continúe con [validación HMAC](/es/desarrolladores/kyc/validar-firma-hmac) y [reentrega y conciliación](/es/desarrolladores/kyc/reentrega-y-conciliacion). Una integración lista también trata firmas inválidas, indisponibilidad temporal y eventos fuera de orden.


---

# Valide la firma HMAC de los webhooks

Source: https://contrato.cloud/es/desarrolladores/kyc/validar-firma-hmac

## Conserve los bytes antes de interpretar JSON

La firma utiliza exactamente `timestamp + '.' + raw_body`, con HMAC SHA-256 y el secreto de webhook de su entorno. Lea el timestamp de `X-Face-Auth-Timestamp` y la firma de `X-Face-Auth-Signature`, con formato `sha256=<hex>`.

No reconstruya el mensaje mediante `JSON.stringify` ni otra serialización del objeto interpretado. Espacios, saltos de línea u orden de propiedades modifican los bytes. Configure su framework para disponer del cuerpo bruto antes del parser JSON.

## Verificación en el backend

El ejemplo recibe una tolerancia temporal definida por su política; no representa una promesa de plazo de entrega. Sincronice el reloj del servidor y evalúe las reentregas junto con su política contra replay.

Verificar firma del webhook

### JavaScript / Node.js

```javascript
import { createHmac, timingSafeEqual } from "node:crypto";
// Read X-Face-Auth-Timestamp / X-Face-Auth-Signature. rawBody is the exact received Buffer.
export function verifyWebhook(rawBody, timestamp, signature, secret, maxAgeSeconds, nowSeconds = Date.now() / 1000) {
  if (!Buffer.isBuffer(rawBody) || !secret || !Number.isFinite(maxAgeSeconds) || maxAgeSeconds <= 0 || !Number.isFinite(nowSeconds)) return false;
  if (typeof timestamp !== "string" || !/^[0-9]{1,16}$/.test(timestamp) || typeof signature !== "string" || !/^sha256=[a-f0-9]{64}$/.test(signature)) return false;
  const sentAt = Number(timestamp);
  if (!Number.isSafeInteger(sentAt) || Math.abs(nowSeconds - sentAt) > maxAgeSeconds) return false;
  const expected = createHmac("sha256", secret).update(Buffer.from(timestamp + ".", "utf8")).update(rawBody).digest();
  const received = Buffer.from(signature.slice(7), "hex");
  return expected.length === received.length && timingSafeEqual(expected, received);
}
// Only after signature verification: parse and validate the envelope, compare X-Face-Auth-Event-Id with event_id, and durably accept with a unique event_id (atomic insert/queue). A duplicate event_id is acknowledged without repeating effects. Do not deduplicate transaction_id. GET the transaction to reconcile current status, decision and decision_reason; delivery order is not authoritative. Acknowledge only after durable acceptance. Persistence, queueing, body limits and business effects belong to your application; this function only verifies HMAC. maxAgeSeconds is YOUR replay policy, not a service delivery promise. Never reserialize JSON before verification or log secrets/payloads.
```

### TypeScript / Node.js

```typescript
import { createHmac, timingSafeEqual } from "node:crypto";
// Read X-Face-Auth-Timestamp / X-Face-Auth-Signature. rawBody is the exact received Buffer.
export function verifyWebhook(rawBody: Buffer, timestamp: string, signature: string, secret: string, maxAgeSeconds: number, nowSeconds = Date.now() / 1000): boolean {
  if (!Buffer.isBuffer(rawBody) || !secret || !Number.isFinite(maxAgeSeconds) || maxAgeSeconds <= 0 || !Number.isFinite(nowSeconds)) return false;
  if (typeof timestamp !== "string" || !/^[0-9]{1,16}$/.test(timestamp) || typeof signature !== "string" || !/^sha256=[a-f0-9]{64}$/.test(signature)) return false;
  const sentAt = Number(timestamp);
  if (!Number.isSafeInteger(sentAt) || Math.abs(nowSeconds - sentAt) > maxAgeSeconds) return false;
  const expected = createHmac("sha256", secret).update(Buffer.from(timestamp + ".", "utf8")).update(rawBody).digest();
  const received = Buffer.from(signature.slice(7), "hex");
  return expected.length === received.length && timingSafeEqual(expected, received);
}
// Only after signature verification: parse and validate the envelope, compare X-Face-Auth-Event-Id with event_id, and durably accept with a unique event_id (atomic insert/queue). A duplicate event_id is acknowledged without repeating effects. Do not deduplicate transaction_id. GET the transaction to reconcile current status, decision and decision_reason; delivery order is not authoritative. Acknowledge only after durable acceptance. Persistence, queueing, body limits and business effects belong to your application; this function only verifies HMAC. maxAgeSeconds is YOUR replay policy, not a service delivery promise. Never reserialize JSON before verification or log secrets/payloads.
```

### Python

```python
from __future__ import annotations
import hashlib
import hmac
import math
import re
import time

def verify_webhook(raw_body: bytes, timestamp: str, signature: str, secret: str,
                   max_age_seconds: float, now_seconds: float | None = None) -> bool:
    now = time.time() if now_seconds is None else now_seconds
    if not isinstance(raw_body, bytes) or not secret or not math.isfinite(max_age_seconds) or max_age_seconds <= 0 or not math.isfinite(now):
        return False
    if not isinstance(timestamp, str) or not re.fullmatch(r"[0-9]{1,16}", timestamp):
        return False
    if not isinstance(signature, str) or not re.fullmatch(r"sha256=[a-f0-9]{64}", signature):
        return False
    sent_at = int(timestamp)
    if sent_at > 9007199254740991 or abs(now - sent_at) > max_age_seconds:
        return False
    expected = "sha256=" + hmac.new(secret.encode("utf-8"), timestamp.encode("utf-8") + b"." + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)

# Only after signature verification: parse and validate the envelope, compare X-Face-Auth-Event-Id with event_id, and durably accept with a unique event_id (atomic insert/queue). A duplicate event_id is acknowledged without repeating effects. Do not deduplicate transaction_id. GET the transaction to reconcile current status, decision and decision_reason; delivery order is not authoritative. Acknowledge only after durable acceptance. Persistence, queueing, body limits and business effects belong to your application; this function only verifies HMAC. maxAgeSeconds is YOUR replay policy, not a service delivery promise. Never reserialize JSON before verification or log secrets/payloads.
```

### PHP

```php
<?php
// rawBody is the unchanged byte string from php://input, before json_decode.
function verifyWebhook(string $rawBody, string $timestamp, string $signature, string $secret, float $maxAgeSeconds, ?float $nowSeconds = null): bool {
    $now = $nowSeconds ?? microtime(true);
    if ($secret === '' || !is_finite($maxAgeSeconds) || $maxAgeSeconds <= 0 || !is_finite($now)) return false;
    if (!preg_match('/^[0-9]{1,16}$/D', $timestamp) || !preg_match('/^sha256=[a-f0-9]{64}$/D', $signature)) return false;
    $sentAt = (float) $timestamp;
    if ($sentAt > 9007199254740991 || abs($now - $sentAt) > $maxAgeSeconds) return false;
    $expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
    return hash_equals($expected, $signature);
}
// Only after signature verification: parse and validate the envelope, compare X-Face-Auth-Event-Id with event_id, and durably accept with a unique event_id (atomic insert/queue). A duplicate event_id is acknowledged without repeating effects. Do not deduplicate transaction_id. GET the transaction to reconcile current status, decision and decision_reason; delivery order is not authoritative. Acknowledge only after durable acceptance. Persistence, queueing, body limits and business effects belong to your application; this function only verifies HMAC. maxAgeSeconds is YOUR replay policy, not a service delivery promise. Never reserialize JSON before verification or log secrets/payloads.
```

### C# / .NET

```csharp
using System;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
// rawBody must be the unchanged request bytes, before JSON deserialization.
public static class Webhook {
  public static bool Verify(byte[] rawBody, string timestamp, string signature, string secret, double maxAgeSeconds, double? nowSeconds = null) {
    double now = nowSeconds ?? DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() / 1000.0;
    if (rawBody is null || string.IsNullOrEmpty(secret) || !double.IsFinite(maxAgeSeconds) || maxAgeSeconds <= 0 || !double.IsFinite(now)) return false;
    if (timestamp is null || signature is null || !Regex.IsMatch(timestamp, @"\A[0-9]{1,16}\z") || !Regex.IsMatch(signature, @"\Asha256=[a-f0-9]{64}\z")) return false;
    if (!long.TryParse(timestamp, out long sentAt) || sentAt > 9007199254740991L || Math.Abs(now - sentAt) > maxAgeSeconds) return false;
    byte[] prefix = Encoding.UTF8.GetBytes(timestamp + ".");
    byte[] signed = new byte[prefix.Length + rawBody.Length];
    Buffer.BlockCopy(prefix, 0, signed, 0, prefix.Length);
    Buffer.BlockCopy(rawBody, 0, signed, prefix.Length, rawBody.Length);
    byte[] expected = HMACSHA256.HashData(Encoding.UTF8.GetBytes(secret), signed);
    return CryptographicOperations.FixedTimeEquals(expected, Convert.FromHexString(signature[7..]));
  }
}
// Only after signature verification: parse and validate the envelope, compare X-Face-Auth-Event-Id with event_id, and durably accept with a unique event_id (atomic insert/queue). A duplicate event_id is acknowledged without repeating effects. Do not deduplicate transaction_id. GET the transaction to reconcile current status, decision and decision_reason; delivery order is not authoritative. Acknowledge only after durable acceptance. Persistence, queueing, body limits and business effects belong to your application; this function only verifies HMAC. maxAgeSeconds is YOUR replay policy, not a service delivery promise. Never reserialize JSON before verification or log secrets/payloads.
```

### Java

```java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.HexFormat;
// Java 17+. rawBody is the original request byte[], before JSON parsing.
class Webhook {
  static boolean verify(byte[] rawBody, String timestamp, String signature, String secret, double maxAgeSeconds, double nowSeconds) throws Exception {
    if (rawBody == null || secret == null || secret.isEmpty() || !Double.isFinite(maxAgeSeconds) || maxAgeSeconds <= 0 || !Double.isFinite(nowSeconds)) return false;
    if (timestamp == null || signature == null || !timestamp.matches("[0-9]{1,16}") || !signature.matches("sha256=[a-f0-9]{64}")) return false;
    long sentAt = Long.parseLong(timestamp);
    if (sentAt > 9007199254740991L || Math.abs(nowSeconds - sentAt) > maxAgeSeconds) return false;
    Mac mac = Mac.getInstance("HmacSHA256");
    mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
    mac.update((timestamp + ".").getBytes(StandardCharsets.UTF_8));
    byte[] expected = mac.doFinal(rawBody);
    return MessageDigest.isEqual(expected, HexFormat.of().parseHex(signature.substring(7)));
  }
}
// Only after signature verification: parse and validate the envelope, compare X-Face-Auth-Event-Id with event_id, and durably accept with a unique event_id (atomic insert/queue). A duplicate event_id is acknowledged without repeating effects. Do not deduplicate transaction_id. GET the transaction to reconcile current status, decision and decision_reason; delivery order is not authoritative. Acknowledge only after durable acceptance. Persistence, queueing, body limits and business effects belong to your application; this function only verifies HMAC. maxAgeSeconds is YOUR replay policy, not a service delivery promise. Never reserialize JSON before verification or log secrets/payloads.
```

### Go

```go
package main
import (
  "crypto/hmac"
  "crypto/sha256"
  "encoding/hex"
  "math"
  "regexp"
  "strconv"
)
// rawBody contains the exact request bytes, before json.Unmarshal. nowSeconds comes from your clock.
func verifyWebhook(rawBody []byte, timestamp, signature, secret string, maxAgeSeconds, nowSeconds float64) bool {
  if len(secret) == 0 || math.IsNaN(maxAgeSeconds) || math.IsInf(maxAgeSeconds, 0) || maxAgeSeconds <= 0 || math.IsNaN(nowSeconds) || math.IsInf(nowSeconds, 0) { return false }
  if !regexp.MustCompile("^[0-9]{1,16}$").MatchString(timestamp) || !regexp.MustCompile("^sha256=[a-f0-9]{64}$").MatchString(signature) { return false }
  sentAt, err := strconv.ParseInt(timestamp, 10, 64)
  if err != nil || sentAt > 9007199254740991 || math.Abs(nowSeconds-float64(sentAt)) > maxAgeSeconds { return false }
  mac := hmac.New(sha256.New, []byte(secret)); mac.Write([]byte(timestamp + ".")); mac.Write(rawBody)
  received, err := hex.DecodeString(signature[7:]); if err != nil { return false }
  return hmac.Equal(mac.Sum(nil), received)
}
// Only after signature verification: parse and validate the envelope, compare X-Face-Auth-Event-Id with event_id, and durably accept with a unique event_id (atomic insert/queue). A duplicate event_id is acknowledged without repeating effects. Do not deduplicate transaction_id. GET the transaction to reconcile current status, decision and decision_reason; delivery order is not authoritative. Acknowledge only after durable acceptance. Persistence, queueing, body limits and business effects belong to your application; this function only verifies HMAC. maxAgeSeconds is YOUR replay policy, not a service delivery promise. Never reserialize JSON before verification or log secrets/payloads.
```

## Después de la validación

Solo después de validar HMAC interprete JSON, valide el envelope y procese el evento. Compruebe la coherencia del identificador entre header y cuerpo. Elimine duplicados mediante `event_id` en almacenamiento duradero; la verificación criptográfica por sí sola no impide repetir efectos de negocio.

Los ejemplos aíslan la validación y no implementan un receptor completo. Añada límites de cuerpo, tratamiento de excepciones, persistencia, colas, logs protegidos y [conciliación](/es/desarrolladores/kyc/reentrega-y-conciliacion). Nunca registre el secreto, cuerpos con datos personales ni enlaces de recorrido en herramientas públicas.


---

# Reentrega, duplicados y conciliación de eventos

Source: https://contrato.cloud/es/desarrolladores/kyc/reentrega-y-conciliacion

## Entrega y procesamiento son problemas diferentes

Una entrega puede fallar, repetirse o llegar tarde. El contrato incluye estados como `retry_pending` y `failed`; no suponga intervalos fijos ni entrega exactamente una vez. Confirme las condiciones operativas de su integración.

1. Valide firma y envelope antes de confiar en el evento.
2. Guarde la aceptación con `event_id` único, evitando carreras entre receptores.
3. Haga idempotente la continuidad de negocio.
4. Consulte la transacción mediante GET para conciliar el estado actual.

Si confirma antes de persistir y luego falla, puede perderse el trabajo. Si persiste y se pierde la confirmación, puede repetirse la entrega. Su diseño debe tratar ambos casos.

## El orden de llegada no define el estado

Un evento `created` retrasado no debe sustituir un estado final ya conocido. El identificador de transacción localiza el recorrido; `event_id` identifica cada evento. Consulte `GET /v1/face-auth/transactions/{transaction_id}` con la clave del Tenant y entorno correctos.

## Prepare la recuperación operativa

Registre identificador, tipo de evento, resultado y fallo sanitizado. No copie cuerpos personales ni credenciales en logs abiertos. Defina cómo su equipo encuentra pendientes, realiza una nueva consulta y evita repetir liberaciones, cobros u otros efectos.

Pruebe duplicados, dos eventos fuera de orden, un fallo antes de persistir y otro después. El [simulador](/es/desarrolladores/kyc/simulador) ayuda a comprender escenarios; sus colas y almacenamiento necesitan pruebas propias.


---

# Descarga autorizada de documentos, assets y evidencias

Source: https://contrato.cloud/es/desarrolladores/kyc/documentos-y-evidencias

## Archivos autorizados en la integración

La API Tenant permite listar assets autorizados en `/assets` y solicitar contenido permitido en `/assets/{asset_id}/content`, después de `/v1/face-auth/transactions/{transaction_id}`. Estas llamadas entre servidores requieren API key, alcance `document_download` e IP autorizada.

Consulte [autenticación](/es/desarrolladores/kyc/autenticacion) y los tipos de respuesta en la [referencia](/es/desarrolladores/kyc/referencia). Utilice los identificadores devueltos por la lista; la autorización del contenido se verifica por separado y la lista no libera evidencia interna.

## El ZIP y el PDF de evidencia están restringidos

Las rutas `/documents/download` y `/evidence.pdf` no ofrecen descargas a la integración Tenant. Incluso con clave válida, alcance e IP autorizada, devuelven `403 evidence_access_restricted`. El acceso a estas evidencias está reservado al Admin interno. La referencia incluye las rutas para explicar la restricción; las solicitudes Postman las excluyen.

No repita la llamada ni amplíe la clave para intentar evitar esta denegación. Conocer un enlace o identificador no concede acceso. Los errores de autenticación, alcance o IP pueden aparecer antes de la restricción de evidencia.

## Proteja el contenido permitido

La clave debe pertenecer al mismo Tenant y entorno que la transacción. Transfiera únicamente contenido autorizado y necesario para la finalidad aprobada. Controle quién puede abrir, exportar y compartir archivos en su aplicación; evite cachés públicas y URLs permanentes sin autorización. Las copias con datos personales también requieren controles y retención adecuada.

Para soporte, facilite identificadores y errores sanitizados, nunca credenciales o documentos por canales abiertos. Los registros disponibles no garantizan conformidad universal ni sustituyen la evaluación jurídica de la contratación.


---

# Errores y diagnóstico de la integración KYC

Source: https://contrato.cloud/es/desarrolladores/kyc/errores-y-diagnostico

## Comience por código, operación y entorno

Las respuestas de error documentadas usan un objeto `detail` con `code` y `message`. Compruebe también el estado HTTP. No trate un JSON de error como una transacción creada correctamente.

| Código | Comprobación recomendada |
| --- | --- |
| `validation_error` | Campos obligatorios, tipos, país, locale y límites |
| `invalid_api_key` | Credencial, revocación y entorno |
| `tenant_environment_disabled` | Habilitación del Tenant y entorno |
| `transaction_not_found` | Identificador, Tenant y entorno correctos |
| `idempotency_conflict` | Misma clave utilizada con payload diferente |
| `tenant_ip_not_authorized` | IP de salida autorizada para la operación |
| `api_key_scope_not_allowed` | Permisos de la clave para el recurso |

## Distinga conexión fallida de rechazo del contrato

Perder la respuesta de creación deja un resultado incierto. Utilice la misma clave de idempotencia y el mismo payload según la [guía de idempotencia](/es/desarrolladores/kyc/idempotencia). Los errores determinísticos de credenciales, alcance o payload requieren corrección; repetir sin límite solo oculta la causa.

Para indisponibilidad temporal, configure timeouts y reintentos limitados en su cliente. No suponga una promesa de disponibilidad ni un límite universal de intentos.

## Comparta un diagnóstico útil

Registre hora, entorno, método, ruta sin secretos, estado HTTP y código sanitizado. Cuando exista, vincule el identificador de transacción a la consulta de soporte. Retire claves, secretos de webhook, datos personales y enlaces de recorrido. Compare su llamada con la [referencia](/es/desarrolladores/kyc/referencia) y los [ejemplos](/es/desarrolladores/kyc/ejemplos-y-descargas) antes de modificar el proceso de negocio.


---

# Límites del contrato y buenas prácticas de integración

Source: https://contrato.cloud/es/desarrolladores/kyc/limites-y-buenas-practicas

## Valide antes de enviar

Utilice los límites del OpenAPI sin crear un segundo contrato en su aplicación. En la creación, `external_id` admite hasta 128 caracteres; `Idempotency-Key`, hasta 180; `metadata`, hasta 8 KB. Si se informa, `expires_in_minutes` varía entre 5 y 129600 minutos. Omitir un campo opcional no equivale a enviar un valor arbitrario.

Consulte países, locales, estados y códigos en la referencia vigente. Por ejemplo, el idioma `en` del sitio no permite sustituir automáticamente un enum de API por ese valor.

## Controle el comportamiento del cliente

Defina timeouts de conexión y respuesta. Limite la concurrencia, supervise su cola y utilice espera progresiva ante fallos transitorios. Corrija errores de autenticación, permisos o payload antes de repetir. Evite consultas continuas sin intervalo para cada transacción.

Esta guía no publica una cuota universal de solicitudes ni un SLA. Confirme con el equipo responsable los límites operativos y comerciales aplicables a su integración.

## Reduzca lo que debe proteger

Envíe únicamente los datos necesarios. No utilice `metadata` como depósito de documentos, secretos o cuerpos personales extensos. Evite registrar las solicitudes completas. Proteja los enlaces de recorrido y archivos descargados como accesos sensibles.

Combine [webhooks](/es/desarrolladores/kyc/webhooks-y-eventos) con [conciliación](/es/desarrolladores/kyc/reentrega-y-conciliacion) en lugar de depender solo de polling frecuente. Consulte la [lista de producción](/es/desarrolladores/kyc/paso-a-produccion) para aplicar estas prácticas a su operación.


---

# Lista para llevar su integración KYC a producción

Source: https://contrato.cloud/es/desarrolladores/kyc/paso-a-produccion

## Confirme entorno y acceso

- Dirección oficial de API confirmada para su entorno habilitado.
- Clave guardada solo en el backend y con permisos necesarios.
- Credenciales y datos separados entre pruebas y producción.
- IPs y alcance de descarga autorizados para acceder a archivos.

## Cubra el ciclo completo

Cree una transacción, guarde la correlación, abra el recorrido devuelto y consulte el resultado. Valide también rechazo, vencimiento y revisión manual; probar solo la aprobación deja decisiones importantes sin tratamiento. Una etapa intermedia no libera el proceso como aprobado.

Simule pérdida de respuesta del POST y confirme el reintento con la misma clave de idempotencia. Envíe duplicados y eventos fuera de orden a su consumidor y compruebe la conciliación mediante GET sin repetir efectos de negocio.

## Revise el receptor de eventos

- Cuerpo bruto conservado antes del parser JSON.
- HMAC y política temporal validados antes del procesamiento.
- Aceptación duradera, deduplicación por `event_id` y recuperación.
- Logs sanitizados, sin secretos en URLs ni herramientas públicas.

## Prepare la operación

Defina quién acompaña pendientes, entregas fallidas y transacciones vencidas. Documente cómo interrumpir la continuidad durante una indisponibilidad, conciliar después y contactar con soporte sin compartir datos personales innecesarios.

Confirme requisitos de finalidad, acceso y retención con las áreas responsables. El simulador verifica comprensión del contrato; no certifica su infraestructura ni realiza una verificación real. El paso a producción depende de la habilitación y validación operativa acordadas para su integración.


---

# Historial y evolución del contrato público KYC

Source: https://contrato.cloud/es/desarrolladores/kyc/historial-de-versiones

## 16 de octubre de 2026 — portal de desarrolladores

Esta edición organiza el contrato B2B KYC en guías, referencia, descargas y simulación local. La especificación de origen declara `info.version=1.0.0`. Esa versión identifica el contrato documentado; publicar guías no anuncia cambios de comportamiento de la API.

La referencia pública documenta cuatro operaciones Tenant compatibles y dos rutas heredadas de evidencia que devuelven `403 evidence_access_restricted`, reservadas al Admin interno. Postman incluye solo las cuatro operaciones compatibles. No publicamos operaciones de Admin, sesiones internas ni controles de infraestructura. El exportador combina la fuente curada con la restricción verificada en el código actual, sin importar la aplicación.

## Cómo evaluar una actualización

Antes de actualizar su consumidor, compare campos obligatorios, enums, errores y autorización. Conserve el tratamiento de los valores documentados y no confunda la revisión de un artículo con la versión de un entorno operativo.

Repita pruebas de creación idempotente, consultas, resultados y webhooks. Compruebe los bytes del HMAC y la deduplicación por evento. Si descarga archivos, confirme también alcances, IPs y tipos de contenido.

## Mantenga un registro de su integración

Guarde la versión de especificación utilizada y la fecha de revisión de su cliente. Un cambio editorial no exige automáticamente una migración. Los cambios incompatibles requieren orientación específica de evolución del contrato.

Descargue la [especificación OpenAPI](/downloads/kyc/openapi.json), consulte los [ejemplos](/es/desarrolladores/kyc/ejemplos-y-descargas) y utilice la [referencia](/es/desarrolladores/kyc/referencia) antes de modificar su código.


---

# Ejemplos de API y descargas para integrar KYC

Source: https://contrato.cloud/es/desarrolladores/kyc/ejemplos-y-descargas

## Archivos del contrato público

- [Especificación OpenAPI en JSON](/downloads/kyc/openapi.json)
- [Colección Postman](/downloads/kyc/postman-collection.json)
- [Entorno Postman sin credenciales](/downloads/kyc/postman-environment.json)

Importe los archivos y configure únicamente en su entorno privado la URL oficial y la clave habilitada. No publique workspaces con secretos. Los ejemplos usan datos ficticios; los timeouts son decisiones demostrativas del cliente, no un SLA.

## Ejemplos HTTP en el backend

Crear transacción KYC

### cURL

```bash
# Server-side only. Configure KYC_API_BASE_URL with your authorized base (https://api.example.invalid is illustrative), and KYC_API_KEY privately. Never expose credentials in browser code.
set -eu
: "${KYC_API_BASE_URL:?Set authorized base}" "${KYC_API_KEY:?Set API key}"
: "${KYC_IDEMPOTENCY_KEY:?Persist the key for this creation intent}"
curl --fail-with-body --max-time 15 --request POST \
  "${KYC_API_BASE_URL%/}/v1/face-auth/transactions" \
  --header "X-API-Key: ${KYC_API_KEY}" \
  --header "Idempotency-Key: ${KYC_IDEMPOTENCY_KEY}" \
  --header 'Content-Type: application/json' \
  --data-binary '{"country_code":"BR","external_id":"docs-example-0001","locale":"pt-BR","subject":{"identity":{"country":"BR","number":"00000000000","type":"CPF"}}}'
# Persist transaction_id and business correlation; use the returned links.webview_url. Read status, decision and decision_reason together. Reuse the SAME idempotency key AND original payload only for the same creation intent; never silently bypass a 409 conflict.
```

### JavaScript / Node.js

```javascript
import process from "node:process";
// Server-side only. Configure KYC_API_BASE_URL with your authorized base (https://api.example.invalid is illustrative), and KYC_API_KEY privately. Never expose credentials in browser code.
function required(name) {
  const value = process.env[name];
  if (!value) throw new Error("Missing configuration: " + name);
  return value;
}
const base = required("KYC_API_BASE_URL").replace(/\/$/, "");
const response = await fetch(base + "/v1/face-auth/transactions", {
  method: "POST",
  headers: { "X-API-Key": required("KYC_API_KEY"), "Content-Type": "application/json", "Idempotency-Key": required("KYC_IDEMPOTENCY_KEY") },
  body: "{\"country_code\":\"BR\",\"external_id\":\"docs-example-0001\",\"locale\":\"pt-BR\",\"subject\":{\"identity\":{\"country\":\"BR\",\"number\":\"00000000000\",\"type\":\"CPF\"}}}",
  signal: AbortSignal.timeout(15_000),
});
if (response.status !== 201) throw new Error("KYC HTTP " + response.status);
const transaction = await response.json();
console.log("HTTP " + response.status);
// Persist transaction_id and business correlation; use the returned links.webview_url. Read status, decision and decision_reason together. Reuse the SAME idempotency key AND original payload only for the same creation intent; never silently bypass a 409 conflict.
```

### TypeScript / Node.js

```typescript
import process from "node:process";
// Server-side only. Configure KYC_API_BASE_URL with your authorized base (https://api.example.invalid is illustrative), and KYC_API_KEY privately. Never expose credentials in browser code.
function required(name: string): string {
  const value = process.env[name];
  if (!value) throw new Error("Missing configuration: " + name);
  return value;
}
const base = required("KYC_API_BASE_URL").replace(/\/$/, "");
const response = await fetch(base + "/v1/face-auth/transactions", {
  method: "POST",
  headers: { "X-API-Key": required("KYC_API_KEY"), "Content-Type": "application/json", "Idempotency-Key": required("KYC_IDEMPOTENCY_KEY") },
  body: "{\"country_code\":\"BR\",\"external_id\":\"docs-example-0001\",\"locale\":\"pt-BR\",\"subject\":{\"identity\":{\"country\":\"BR\",\"number\":\"00000000000\",\"type\":\"CPF\"}}}",
  signal: AbortSignal.timeout(15_000),
});
if (response.status !== 201) throw new Error("KYC HTTP " + response.status);
const transaction: unknown = await response.json();
// Validate the response against the curated OpenAPI before using it.
console.log("HTTP " + response.status);
// Persist transaction_id and business correlation; use the returned links.webview_url. Read status, decision and decision_reason together. Reuse the SAME idempotency key AND original payload only for the same creation intent; never silently bypass a 409 conflict.
```

### Python

```python
import json
import os
from urllib.parse import quote
from urllib.request import Request, urlopen
# Server-side only. Configure KYC_API_BASE_URL with your authorized base (https://api.example.invalid is illustrative), and KYC_API_KEY privately. Never expose credentials in browser code.
base = os.environ["KYC_API_BASE_URL"].rstrip("/")
request = Request(base + "/v1/face-auth/transactions",
    method="POST",
    headers={"X-API-Key": os.environ["KYC_API_KEY"], "Content-Type": "application/json",
             "Idempotency-Key": os.environ["KYC_IDEMPOTENCY_KEY"]},
    data="{\"country_code\":\"BR\",\"external_id\":\"docs-example-0001\",\"locale\":\"pt-BR\",\"subject\":{\"identity\":{\"country\":\"BR\",\"number\":\"00000000000\",\"type\":\"CPF\"}}}".encode("utf-8"),
)
with urlopen(request, timeout=15) as response:
    if response.status != 201:
        raise RuntimeError("KYC HTTP " + str(response.status))
    transaction = json.load(response)
    print("HTTP " + str(response.status))
# Persist transaction_id and business correlation; use the returned links.webview_url. Read status, decision and decision_reason together. Reuse the SAME idempotency key AND original payload only for the same creation intent; never silently bypass a 409 conflict.
```

### PHP

```php
<?php
// PHP 8+, standard HTTP streams. Server-side only. Configure KYC_API_BASE_URL with your authorized base (https://api.example.invalid is illustrative), and KYC_API_KEY privately. Never expose credentials in browser code.
function required(string $name): string {
    $value = getenv($name);
    if ($value === false || $value === '') throw new RuntimeException("Missing configuration: " . $name);
    return $value;
}
$url = rtrim(required('KYC_API_BASE_URL'), '/') . '/v1/face-auth/transactions';
$headers = ['X-API-Key: ' . required('KYC_API_KEY'), 'Content-Type: application/json', 'Idempotency-Key: ' . required('KYC_IDEMPOTENCY_KEY')];
$context = stream_context_create(['http' => ['method' => 'POST', 'header' => implode("\r\n", $headers),
    'timeout' => 15, 'ignore_errors' => true, 'content' => '{"country_code":"BR","external_id":"docs-example-0001","locale":"pt-BR","subject":{"identity":{"country":"BR","number":"00000000000","type":"CPF"}}}']]);
$body = file_get_contents($url, false, $context);
if ($body === false) throw new RuntimeException('KYC transport failure');
if (!preg_match('/^HTTP\/\S+ ([0-9]{3})/', $http_response_header[0] ?? '', $matches)) throw new RuntimeException('Invalid HTTP response');
$status = (int) $matches[1];
if ($status !== 201) throw new RuntimeException('KYC HTTP ' . $status);
$transaction = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
echo 'HTTP ' . $status . PHP_EOL;
// Persist transaction_id and business correlation; use the returned links.webview_url. Read status, decision and decision_reason together. Reuse the SAME idempotency key AND original payload only for the same creation intent; never silently bypass a 409 conflict.
```

### C# / .NET

```csharp
using System;
using System.Net.Http;
using System.Text;
using System.Text.Json;
// .NET 8+. Server-side only. Configure KYC_API_BASE_URL with your authorized base (https://api.example.invalid is illustrative), and KYC_API_KEY privately. Never expose credentials in browser code.
static string Required(string name) => Environment.GetEnvironmentVariable(name) is { Length: > 0 } value ? value : throw new Exception("Missing configuration: " + name);
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(15) };
using var request = new HttpRequestMessage(HttpMethod.Post, Required("KYC_API_BASE_URL").TrimEnd('/') + "/v1/face-auth/transactions");
request.Headers.Add("X-API-Key", Required("KYC_API_KEY"));
request.Headers.Add("Idempotency-Key", Required("KYC_IDEMPOTENCY_KEY"));
request.Content = new StringContent("{\"country_code\":\"BR\",\"external_id\":\"docs-example-0001\",\"locale\":\"pt-BR\",\"subject\":{\"identity\":{\"country\":\"BR\",\"number\":\"00000000000\",\"type\":\"CPF\"}}}", Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
if ((int)response.StatusCode != 201) throw new Exception("KYC HTTP " + (int)response.StatusCode);
using var transaction = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
Console.WriteLine("HTTP " + (int)response.StatusCode);
// Persist transaction_id and business correlation; use the returned links.webview_url. Read status, decision and decision_reason together. Reuse the SAME idempotency key AND original payload only for the same creation intent; never silently bypass a 409 conflict.
```

### Java

```java
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.*;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
// Java 17+. Server-side only. Configure KYC_API_BASE_URL with your authorized base (https://api.example.invalid is illustrative), and KYC_API_KEY privately. Never expose credentials in browser code.
class Example {
  static String required(String name) {
    String value = System.getenv(name);
    if (value == null || value.isEmpty()) throw new IllegalStateException("Missing configuration: " + name);
    return value;
  }
  public static void main(String[] args) throws Exception {
    String base = required("KYC_API_BASE_URL").replaceAll("/+$", "");
    String url = base + "/v1/face-auth/transactions";
    var builder = HttpRequest.newBuilder(URI.create(url)).timeout(Duration.ofSeconds(15))
        .header("X-API-Key", required("KYC_API_KEY"));
    builder.header("Idempotency-Key", required("KYC_IDEMPOTENCY_KEY"))
        .header("Content-Type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString("{\"country_code\":\"BR\",\"external_id\":\"docs-example-0001\",\"locale\":\"pt-BR\",\"subject\":{\"identity\":{\"country\":\"BR\",\"number\":\"00000000000\",\"type\":\"CPF\"}}}", StandardCharsets.UTF_8));
    var response = HttpClient.newHttpClient().send(builder.build(), HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
    if (response.statusCode() != 201) throw new IllegalStateException("KYC HTTP " + response.statusCode());
    System.out.println("HTTP " + response.statusCode());
    // Parse response.body() with your application's JSON parser and validate against OpenAPI.
    // Persist transaction_id and business correlation; use the returned links.webview_url. Read status, decision and decision_reason together. Reuse the SAME idempotency key AND original payload only for the same creation intent; never silently bypass a 409 conflict.
  }
}
```

### Go

```go
package main
import (
  "encoding/json"
  "fmt"
  "net/http"
  "net/url"
  "os"
  "strings"
  "time"
)
// Server-side only. Configure KYC_API_BASE_URL with your authorized base (https://api.example.invalid is illustrative), and KYC_API_KEY privately. Never expose credentials in browser code.
func required(name string) string {
  value := os.Getenv(name); if value == "" { panic("Missing configuration: " + name) }; return value
}
func main() {
  _ = url.PathEscape // Used by the GET variant.
  endpoint := strings.TrimRight(required("KYC_API_BASE_URL"), "/") + "/v1/face-auth/transactions"
  request, err := http.NewRequest("POST", endpoint, strings.NewReader(`{"country_code":"BR","external_id":"docs-example-0001","locale":"pt-BR","subject":{"identity":{"country":"BR","number":"00000000000","type":"CPF"}}}`)); if err != nil { panic(err) }
  request.Header.Set("X-API-Key", required("KYC_API_KEY"))
  request.Header.Set("Content-Type", "application/json")
  request.Header.Set("Idempotency-Key", required("KYC_IDEMPOTENCY_KEY"))
  response, err := (&http.Client{Timeout: 15*time.Second}).Do(request); if err != nil { panic(err) }; defer response.Body.Close()
  if response.StatusCode != 201 { panic(fmt.Sprintf("KYC HTTP %d", response.StatusCode)) }
  var transaction map[string]any
  if err := json.NewDecoder(response.Body).Decode(&transaction); err != nil { panic(err) }
  fmt.Printf("HTTP %d\n", response.StatusCode)
  // Persist transaction_id and business correlation; use the returned links.webview_url. Read status, decision and decision_reason together. Reuse the SAME idempotency key AND original payload only for the same creation intent; never silently bypass a 409 conflict.
}
```


Estos fragmentos muestran la creación básica; no son SDKs oficiales ni consumidores completos. Añada validación de configuración, persistencia, tratamiento de errores y reintentos idempotentes según su aplicación. Una nueva intención de creación requiere otra clave; un reintento técnico conserva la anterior y el mismo payload.

Para cURL, consulte el [inicio rápido](/es/desarrolladores/kyc/inicio-rapido). Para su receptor de eventos, siga [validación HMAC](/es/desarrolladores/kyc/validar-firma-hmac) y [conciliación](/es/desarrolladores/kyc/reentrega-y-conciliacion). El [simulador](/es/desarrolladores/kyc/simulador) demuestra escenarios fijos sin acceder a la API real.


---

# Explore el simulador local de KYC

Source: https://contrato.cloud/es/desarrolladores/kyc/guia-del-simulador

## Comprenda el contrato antes de integrar

El [simulador local](/es/desarrolladores/kyc/simulador) permite explorar escenarios fijos de creación y resultado. Funciona en el navegador con datos sintéticos. No solicita documento de identidad, selfie, clave de API ni secreto de webhook y no realiza verificaciones de identidad.

Utilícelo para alinear producto e ingeniería sobre qué debe hacer su sistema cuando un recorrido queda pendiente, es aprobado, se rechaza, vence o requiere revisión manual. Observe la diferencia entre `status` y `decision` en lugar de liberar un proceso por una etapa intermedia.

## Un recorrido de exploración

1. Comience por la creación e identifique el `transaction_id` sintético.
2. Seleccione los resultados disponibles y compare sus estados.
3. Relacione cada escenario con una pantalla o acción de su sistema.
4. Lea por qué los duplicados y eventos retrasados requieren conciliación.

Anote qué decisiones necesitan intervención humana y cuáles pueden continuar automáticamente según su política. El objetivo es transformar un payload técnico en una regla operativa comprensible.

## Qué debe probar su integración

El simulador no certifica credenciales, red, entrega de mensajes, biometría ni persistencia. Esos comportamientos requieren validación en un entorno habilitado y en la infraestructura de su aplicación. No envíe datos reales para ampliar la prueba.

Continúe con el [inicio rápido](/es/desarrolladores/kyc/inicio-rapido), valide su consumidor de [webhooks](/es/desarrolladores/kyc/webhooks-y-eventos) y complete la [lista de producción](/es/desarrolladores/kyc/paso-a-produccion).
