## Define the process being verified

Create a transaction with `POST /v1/face-auth/transactions`. Required fields are `country_code` and `subject`; inside `subject`, provide `identity` with `type`, `number` and `country`. Check supported countries and locales in the reference; the website’s language list is not the API’s enum list.

Create a KYC transaction

### 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.
}
```

## Correlate without duplicating data

Use `external_id` to relate the process in your system. `subject.tenant_subject_id` identifies the person in your database. These identifiers serve different purposes and do not replace the `transaction_id` returned by the API.

`source` describes the originating system, integration and channel. `metadata` holds business context, limited to 8 KB; avoid sensitive or unnecessary data. Set expiration with `expires_in_minutes` within the contract’s bounds; omitting it uses the Tenant default.

## Handle the response

The documented response is `201`, including creation recovered through idempotency. Persist the identifier before directing the person to the journey. If the response is lost after sending the request, repeat the same creation with the same key and payload; do not automatically generate a new key.

When provided, `callback_url` sets the webhook for that transaction; otherwise, the Tenant configuration applies. Review [idempotency](/en/developers/kyc/idempotency) and [webhooks](/en/developers/kyc/webhooks-and-events) before moving this workflow into production.
