# Создать контакт для домена `POST /contacts` **operationId:** `createContact` Создаёт локальную карточку. Контакт реестра создаётся/связывается при доменных операциях, не этой командой. Для person обязательно отчество. residence_country_code и postal_code обязательны несмотря на необязательность в старых примерах. Правила документа зависят от страны резидентства, не от среды ключа. **Scopes:** `contacts.write` ## Параметры ### Idempotency-Key Расположение: `header`. Обязательный. Обязателен для POST/PUT/PATCH/DELETE, включая check, sync, test, rotate-secret. После trim 1..160 символов. Один ключ на логическую операцию и API-ключ. Повторите те же method/path/query и байты JSON. TTL HTTP replay по умолчанию 24 часа от первого резервирования; не продлевается. Не переиспользуйте ключ для новой операции, доменная задача дополнительно сохраняет связь с ним. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | minLength: 1; maxLength: 160 | | **Пример** ```json "webhook-create-20261005-0001" ``` ## Тело запроса Обязательное. ### application/json | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["type","email","phone","country_code","residence_country_code","city","address_line","postal_code"] | POST и PATCH используют одинаковую полную валидацию: PATCH не частичный. Неуказанные необязательные текстовые поля обнуляются; external_id сохраняется при отсутствии. Документ при отсутствии обоих полей сохраняется. customer_uuid обязателен в managed. Схема: CoreUpsertContactRequest | | `$.customer_uuid` | string / null | Нет | format: "uuid" | Обязателен в managed. При aggregate можно опустить для агрегированного покупателя. | | `$.external_id` | string / null | Нет | maxLength: 255 | Идентификатор во внешнем биллинге; уникален для реселлера+среды. | | `$.type` | string | Да | enum: "person", "organization" | Тип контакта. | | `$.first_name` | string / null | Нет | maxLength: 255 | Обязателен для person. Unicode-буквы/диакритика, одиночные пробелы, дефис или апостроф между словами. | | `$.last_name` | string / null | Нет | maxLength: 255 | Обязателен для person; те же правила имени. | | `$.middle_name` | string / null | Нет | maxLength: 255 | В текущем API отчество ОБЯЗАТЕЛЬНО для person, не только имя и фамилия. | | `$.organization_name` | string / null | Нет | maxLength: 500 | Обязательно для organization. | | `$.email` | string | Да | format: "email"; maxLength: 255 | Email. | | `$.phone` | string | Да | maxLength: 50 | Телефон. Контроллер ограничивает длину, не проверяет E.164; используйте международный формат. | | `$.country_code` | string | Да | minLength: 2; maxLength: 2 | Страна адреса, верхний регистр сохраняется сервером. | | `$.residence_country_code` | string | Да | minLength: 2; maxLength: 2 | Обязательная страна резидентства; определяет правила документа. | | `$.city` | string | Да | maxLength: 255 | Город. | | `$.region` | string / null | Нет | maxLength: 255 | Регион. | | `$.address_line` | string | Да | maxLength: 1000 | Адрес. | | `$.postal_code` | string | Да | maxLength: 30 | Обязательный индекс. | | `$.external_id_type` | string / null | Нет | maxLength: 40 | Обязателен с external_id_value. KZ: BIN/IIN (12 цифр и контрольная сумма); RU: INN (10/12 цифр); US: EIN (NN-NNNNNNN), SSN (NNN-NN-NNNN); CN: USCC (18 символов); DE: VAT (DE+9 цифр); UZ: STIR (9), PINFL (14); GB: CRN (8), VAT (GB+9); TR: VKN (10), TCKN (11); UA: EDRPOU (8); AE: TRN (100+12). Для остальных стран PASSPORT/TAX_ID (непустое значение). USCC допускает [0-9A-HJ-NP-RT-UW-Y]{18}. Форматы регистрозависимы для значения документа, тип/страна нормализуются к верхнему регистру. | | `$.external_id_value` | string / null | Нет | maxLength: 100; Только запись | Обязателен с external_id_type; хранится, но не возвращается. Для KZ проверяется контрольная сумма; тестовые исключения зависят от конфигурации, test-ключ не отключает проверку. | | `$ (allOf 1)` | Условная схема | Да | | | | `$ (allOf 1) (if)` | object | Да | | | | `$ (allOf 1) (if).type` | Условная схема | Нет | const: "person" | | | `$ (allOf 1) (then)` | object | Да | required: ["first_name","last_name","middle_name"] | | | `$ (allOf 1) (then).first_name` | string | Да | minLength: 1 | | | `$ (allOf 1) (then).last_name` | string | Да | minLength: 1 | | | `$ (allOf 1) (then).middle_name` | string | Да | minLength: 1 | | | `$ (allOf 2)` | Условная схема | Да | | | | `$ (allOf 2) (if)` | object | Да | | | | `$ (allOf 2) (if).type` | Условная схема | Нет | const: "organization" | | | `$ (allOf 2) (then)` | object | Да | required: ["organization_name"] | | | `$ (allOf 2) (then).organization_name` | string | Да | minLength: 1 | | #### Полный контакт физического лица ```json { "type": "person", "first_name": "Иван", "last_name": "Петров", "middle_name": "Иванович", "organization_name": null, "email": "ivan@example.com", "phone": "+77010000000", "country_code": "KZ", "residence_country_code": "KZ", "city": "Алматы", "region": "Алматы", "address_line": "ул. Примерная, 10", "postal_code": "050000", "external_id": "contact-1042", "customer_uuid": "019a0000-0000-7000-8000-000000000010" } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request POST "$API_BASE_URL/contacts" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "type": "person", "first_name": "Иван", "last_name": "Петров", "middle_name": "Иванович", "organization_name": null, "email": "ivan@example.com", "phone": "+77010000000", "country_code": "KZ", "residence_country_code": "KZ", "city": "Алматы", "region": "Алматы", "address_line": "ул. Примерная, 10", "postal_code": "050000", "external_id": "contact-1042", "customer_uuid": "019a0000-0000-7000-8000-000000000010" }' ``` ```php [PHP 8+] 'POST', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/contacts"; const url = baseUrl + path; const body = `{ "type": "person", "first_name": "Иван", "last_name": "Петров", "middle_name": "Иванович", "organization_name": null, "email": "ivan@example.com", "phone": "+77010000000", "country_code": "KZ", "residence_country_code": "KZ", "city": "Алматы", "region": "Алматы", "address_line": "ул. Примерная, 10", "postal_code": "050000", "external_id": "contact-1042", "customer_uuid": "019a0000-0000-7000-8000-000000000010" }`; const response = await fetch(url, { method: "POST", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/contacts" url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"type\": \"person\",\n \"first_name\": \"Иван\",\n \"last_name\": \"Петров\",\n \"middle_name\": \"Иванович\",\n \"organization_name\": null,\n \"email\": \"ivan@example.com\",\n \"phone\": \"+77010000000\",\n \"country_code\": \"KZ\",\n \"residence_country_code\": \"KZ\",\n \"city\": \"Алматы\",\n \"region\": \"Алматы\",\n \"address_line\": \"ул. Примерная, 10\",\n \"postal_code\": \"050000\",\n \"external_id\": \"contact-1042\",\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000010\"\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="POST") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/contacts" endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("POST", endpoint, strings.NewReader("{\n \"type\": \"person\",\n \"first_name\": \"Иван\",\n \"last_name\": \"Петров\",\n \"middle_name\": \"Иванович\",\n \"organization_name\": null,\n \"email\": \"ivan@example.com\",\n \"phone\": \"+77010000000\",\n \"country_code\": \"KZ\",\n \"residence_country_code\": \"KZ\",\n \"city\": \"Алматы\",\n \"region\": \"Алматы\",\n \"address_line\": \"ул. Примерная, 10\",\n \"postal_code\": \"050000\",\n \"external_id\": \"contact-1042\",\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000010\"\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/contacts"; URI uri = URI.create(baseUrl + path); String body = "{\n \"type\": \"person\",\n \"first_name\": \"Иван\",\n \"last_name\": \"Петров\",\n \"middle_name\": \"Иванович\",\n \"organization_name\": null,\n \"email\": \"ivan@example.com\",\n \"phone\": \"+77010000000\",\n \"country_code\": \"KZ\",\n \"residence_country_code\": \"KZ\",\n \"city\": \"Алматы\",\n \"region\": \"Алматы\",\n \"address_line\": \"ул. Примерная, 10\",\n \"postal_code\": \"050000\",\n \"external_id\": \"contact-1042\",\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000010\"\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("POST", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/contacts"; using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("POST"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"type\": \"person\",\n \"first_name\": \"Иван\",\n \"last_name\": \"Петров\",\n \"middle_name\": \"Иванович\",\n \"organization_name\": null,\n \"email\": \"ivan@example.com\",\n \"phone\": \"+77010000000\",\n \"country_code\": \"KZ\",\n \"residence_country_code\": \"KZ\",\n \"city\": \"Алматы\",\n \"region\": \"Алматы\",\n \"address_line\": \"ул. Примерная, 10\",\n \"postal_code\": \"050000\",\n \"external_id\": \"contact-1042\",\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000010\"\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: #### Контакт с ИИН; синтетический пример с корректной контрольной суммой, замените данными владельца ```json { "customer_uuid": "019a0000-0000-7000-8000-000000000010", "external_id": "contact-1044", "type": "person", "first_name": "Иван", "last_name": "Петров", "middle_name": "Иванович", "email": "ivan@example.com", "phone": "+77010000000", "country_code": "KZ", "residence_country_code": "KZ", "city": "Алматы", "address_line": "ул. Примерная, 10", "postal_code": "050000", "external_id_type": "IIN", "external_id_value": "900101300116" } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request POST "$API_BASE_URL/contacts" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "customer_uuid": "019a0000-0000-7000-8000-000000000010", "external_id": "contact-1044", "type": "person", "first_name": "Иван", "last_name": "Петров", "middle_name": "Иванович", "email": "ivan@example.com", "phone": "+77010000000", "country_code": "KZ", "residence_country_code": "KZ", "city": "Алматы", "address_line": "ул. Примерная, 10", "postal_code": "050000", "external_id_type": "IIN", "external_id_value": "900101300116" }' ``` ```php [PHP 8+] 'POST', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/contacts"; const url = baseUrl + path; const body = `{ "customer_uuid": "019a0000-0000-7000-8000-000000000010", "external_id": "contact-1044", "type": "person", "first_name": "Иван", "last_name": "Петров", "middle_name": "Иванович", "email": "ivan@example.com", "phone": "+77010000000", "country_code": "KZ", "residence_country_code": "KZ", "city": "Алматы", "address_line": "ул. Примерная, 10", "postal_code": "050000", "external_id_type": "IIN", "external_id_value": "900101300116" }`; const response = await fetch(url, { method: "POST", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/contacts" url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000010\",\n \"external_id\": \"contact-1044\",\n \"type\": \"person\",\n \"first_name\": \"Иван\",\n \"last_name\": \"Петров\",\n \"middle_name\": \"Иванович\",\n \"email\": \"ivan@example.com\",\n \"phone\": \"+77010000000\",\n \"country_code\": \"KZ\",\n \"residence_country_code\": \"KZ\",\n \"city\": \"Алматы\",\n \"address_line\": \"ул. Примерная, 10\",\n \"postal_code\": \"050000\",\n \"external_id_type\": \"IIN\",\n \"external_id_value\": \"900101300116\"\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="POST") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/contacts" endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("POST", endpoint, strings.NewReader("{\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000010\",\n \"external_id\": \"contact-1044\",\n \"type\": \"person\",\n \"first_name\": \"Иван\",\n \"last_name\": \"Петров\",\n \"middle_name\": \"Иванович\",\n \"email\": \"ivan@example.com\",\n \"phone\": \"+77010000000\",\n \"country_code\": \"KZ\",\n \"residence_country_code\": \"KZ\",\n \"city\": \"Алматы\",\n \"address_line\": \"ул. Примерная, 10\",\n \"postal_code\": \"050000\",\n \"external_id_type\": \"IIN\",\n \"external_id_value\": \"900101300116\"\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/contacts"; URI uri = URI.create(baseUrl + path); String body = "{\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000010\",\n \"external_id\": \"contact-1044\",\n \"type\": \"person\",\n \"first_name\": \"Иван\",\n \"last_name\": \"Петров\",\n \"middle_name\": \"Иванович\",\n \"email\": \"ivan@example.com\",\n \"phone\": \"+77010000000\",\n \"country_code\": \"KZ\",\n \"residence_country_code\": \"KZ\",\n \"city\": \"Алматы\",\n \"address_line\": \"ул. Примерная, 10\",\n \"postal_code\": \"050000\",\n \"external_id_type\": \"IIN\",\n \"external_id_value\": \"900101300116\"\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("POST", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/contacts"; using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("POST"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000010\",\n \"external_id\": \"contact-1044\",\n \"type\": \"person\",\n \"first_name\": \"Иван\",\n \"last_name\": \"Петров\",\n \"middle_name\": \"Иванович\",\n \"email\": \"ivan@example.com\",\n \"phone\": \"+77010000000\",\n \"country_code\": \"KZ\",\n \"residence_country_code\": \"KZ\",\n \"city\": \"Алматы\",\n \"address_line\": \"ул. Примерная, 10\",\n \"postal_code\": \"050000\",\n \"external_id_type\": \"IIN\",\n \"external_id_value\": \"900101300116\"\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: #### Полный контакт организации ```json { "type": "organization", "first_name": null, "last_name": null, "middle_name": null, "organization_name": "ТОО \"Example\"", "email": "admin@example.com", "phone": "+77010000000", "country_code": "KZ", "residence_country_code": "KZ", "city": "Алматы", "region": "Алматы", "address_line": "ул. Примерная, 10", "postal_code": "050000", "external_id": "contact-1043", "customer_uuid": "019a0000-0000-7000-8000-000000000011" } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request POST "$API_BASE_URL/contacts" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "type": "organization", "first_name": null, "last_name": null, "middle_name": null, "organization_name": "ТОО \"Example\"", "email": "admin@example.com", "phone": "+77010000000", "country_code": "KZ", "residence_country_code": "KZ", "city": "Алматы", "region": "Алматы", "address_line": "ул. Примерная, 10", "postal_code": "050000", "external_id": "contact-1043", "customer_uuid": "019a0000-0000-7000-8000-000000000011" }' ``` ```php [PHP 8+] 'POST', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/contacts"; const url = baseUrl + path; const body = `{ "type": "organization", "first_name": null, "last_name": null, "middle_name": null, "organization_name": "ТОО \\"Example\\"", "email": "admin@example.com", "phone": "+77010000000", "country_code": "KZ", "residence_country_code": "KZ", "city": "Алматы", "region": "Алматы", "address_line": "ул. Примерная, 10", "postal_code": "050000", "external_id": "contact-1043", "customer_uuid": "019a0000-0000-7000-8000-000000000011" }`; const response = await fetch(url, { method: "POST", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/contacts" url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"type\": \"organization\",\n \"first_name\": null,\n \"last_name\": null,\n \"middle_name\": null,\n \"organization_name\": \"ТОО \\\"Example\\\"\",\n \"email\": \"admin@example.com\",\n \"phone\": \"+77010000000\",\n \"country_code\": \"KZ\",\n \"residence_country_code\": \"KZ\",\n \"city\": \"Алматы\",\n \"region\": \"Алматы\",\n \"address_line\": \"ул. Примерная, 10\",\n \"postal_code\": \"050000\",\n \"external_id\": \"contact-1043\",\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000011\"\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="POST") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/contacts" endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("POST", endpoint, strings.NewReader("{\n \"type\": \"organization\",\n \"first_name\": null,\n \"last_name\": null,\n \"middle_name\": null,\n \"organization_name\": \"ТОО \\\"Example\\\"\",\n \"email\": \"admin@example.com\",\n \"phone\": \"+77010000000\",\n \"country_code\": \"KZ\",\n \"residence_country_code\": \"KZ\",\n \"city\": \"Алматы\",\n \"region\": \"Алматы\",\n \"address_line\": \"ул. Примерная, 10\",\n \"postal_code\": \"050000\",\n \"external_id\": \"contact-1043\",\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000011\"\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/contacts"; URI uri = URI.create(baseUrl + path); String body = "{\n \"type\": \"organization\",\n \"first_name\": null,\n \"last_name\": null,\n \"middle_name\": null,\n \"organization_name\": \"ТОО \\\"Example\\\"\",\n \"email\": \"admin@example.com\",\n \"phone\": \"+77010000000\",\n \"country_code\": \"KZ\",\n \"residence_country_code\": \"KZ\",\n \"city\": \"Алматы\",\n \"region\": \"Алматы\",\n \"address_line\": \"ул. Примерная, 10\",\n \"postal_code\": \"050000\",\n \"external_id\": \"contact-1043\",\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000011\"\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("POST", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/contacts"; using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("POST"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"type\": \"organization\",\n \"first_name\": null,\n \"last_name\": null,\n \"middle_name\": null,\n \"organization_name\": \"ТОО \\\"Example\\\"\",\n \"email\": \"admin@example.com\",\n \"phone\": \"+77010000000\",\n \"country_code\": \"KZ\",\n \"residence_country_code\": \"KZ\",\n \"city\": \"Алматы\",\n \"region\": \"Алматы\",\n \"address_line\": \"ул. Примерная, 10\",\n \"postal_code\": \"050000\",\n \"external_id\": \"contact-1043\",\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000011\"\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Bearer-ключ отсутствует, недействителен, отозван или истёк. | Проверьте ключ и его срок, не повторяйте с теми же неверными credentials. | | 403 | `forbidden` | Scope, IP allowlist, режим или состояние реселлера не разрешает запрос. | Проверьте scopes, среду, allowlist и настройки реселлера через администратора. | | 404 | `resource_not_found` | Маршрут отключён конфигурацией либо объект недоступен в текущем контексте. | Проверьте UUID, среду и принадлежность; при отключённой функции обратитесь к оператору. | | 409 | `conflict` | Idempotency-Key уже обрабатывается либо использован для другого метода, URL/query или тела. | Для того же запроса дождитесь завершения и повторите неизменённый запрос с тем же ключом; изменённый запрос требует нового ключа после сверки результата. | | 422 | `validation_failed` | Отсутствуют обязательные поля (включая middle_name для person), неверный документ/контрольная сумма, customer_uuid не разрешён, external_id уже занят. | Исправьте поля из error.details/условия. Не повторяйте неизменённый невалидный запрос. | | 429 | `rate_limit_exceeded` | Лимит текущего API-ключа исчерпан. | Учитывайте Retry-After при наличии, используйте backoff с jitter. Запись повторяйте с прежним Idempotency-Key и неизменённым телом. | | 500 | `internal_error` | Необработанная ошибка выполнения; это не подтверждение отсутствия побочного эффекта. | Сохраните request_id; сначала проверьте состояние объекта, затем повторяйте запись с тем же ключом. Для неоднозначного результата обращайтесь в поддержку. | ### HTTP 201 Успешный ответ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `X-RateLimit-Limit`**: Лимит API-ключа на 60 секунд. После прохождения ограничителя, включая replay. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 1 | | ```text X-RateLimit-Limit: 120 ``` **Заголовок `X-RateLimit-Remaining`**: Остаток после запроса. Снимок, не гарантия при конкуренции. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 0 | | ```text X-RateLimit-Remaining: 119 ``` **Заголовок `Idempotency-Replayed`**: Только при возврате сохранённого HTTP-ответа: true, не false. Отсутствие не доказывает, что доменная операция ранее не выполнялась. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | const: "true" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data"] | Успешный ответ. Схема: CoreContactEnvelope | | `$.data` | object | Да | required: ["uuid","type","status","first_name","last_name","middle_name","organization_name","email","phone","country_code","residence_country_code","city","region","address_line","postal_code","external_id_type","verification_status","is_locked_as_registrant","verified_at","external_id","customer_uuid"] | Локальный доменный контакт. external_id_value никогда не возвращается. Изменение этой карточки само по себе не выполняет EPP contact update. Схема: CoreContact | | `$.data.uuid` | string | Да | format: "uuid" | UUID объекта. | | `$.data.type` | string | Да | enum: "person", "organization" | Тип. | | `$.data.status` | string | Да | | Статус, при создании active. | | `$.data.first_name` | string / null | Да | | Имя. | | `$.data.last_name` | string / null | Да | | Фамилия. | | `$.data.middle_name` | string / null | Да | | Отчество. | | `$.data.organization_name` | string / null | Да | | Организация. | | `$.data.email` | string | Да | format: "email" | Email. | | `$.data.phone` | string | Да | | Телефон. | | `$.data.country_code` | string | Да | | Страна адреса. | | `$.data.residence_country_code` | string / null | Да | | Страна резидентства. | | `$.data.city` | string | Да | | Город. | | `$.data.region` | string / null | Да | | Регион. | | `$.data.address_line` | string | Да | | Адрес. | | `$.data.postal_code` | string / null | Да | | Почтовый индекс. | | `$.data.external_id_type` | string / null | Да | | Тип документа, не идентификатор внешнего биллинга. | | `$.data.verification_status` | string | Да | | Статус верификации; создание обычно not_required. | | `$.data.is_locked_as_registrant` | boolean | Да | | Признак блокировки контакта как владельца. | | `$.data.verified_at` | string / null | Да | format: "date-time" | Время подтверждения. | | `$.data.external_id` | string / null | Да | | Идентификатор контакта во внешнем биллинге. | | `$.data.customer_uuid` | string / null | Да | format: "uuid" | UUID покупателя. | #### Физическое лицо без документа; для регистрации документ может понадобиться (201) ```json { "data": { "uuid": "019a0000-0000-7000-8000-000000000012", "type": "person", "status": "active", "first_name": "Иван", "last_name": "Петров", "middle_name": "Иванович", "organization_name": null, "email": "ivan@example.com", "phone": "+77010000000", "country_code": "KZ", "residence_country_code": "KZ", "city": "Алматы", "region": "Алматы", "address_line": "ул. Примерная, 10", "postal_code": "050000", "external_id_type": null, "verification_status": "not_required", "is_locked_as_registrant": false, "verified_at": null, "external_id": "contact-1042", "customer_uuid": "019a0000-0000-7000-8000-000000000010" } } ``` #### Организация (201) ```json { "data": { "uuid": "019a0000-0000-7000-8000-000000000013", "type": "organization", "status": "active", "first_name": null, "last_name": null, "middle_name": null, "organization_name": "ТОО \"Example\"", "email": "admin@example.com", "phone": "+77010000000", "country_code": "KZ", "residence_country_code": "KZ", "city": "Алматы", "region": "Алматы", "address_line": "ул. Примерная, 10", "postal_code": "050000", "external_id_type": null, "verification_status": "not_required", "is_locked_as_registrant": false, "verified_at": null, "external_id": "contact-1043", "customer_uuid": "019a0000-0000-7000-8000-000000000011" } } ``` ### HTTP 401 Bearer-ключ отсутствует, недействителен, отозван или истёк. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Scope, IP allowlist, режим или состояние реселлера не разрешает запрос. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Маршрут отключён конфигурацией либо объект недоступен в текущем контексте. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 409 Idempotency-Key уже обрабатывается либо использован для другого метода, URL/query или тела. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (409) ```json { "error": { "code": "conflict", "message": "A request with this Idempotency-Key is already processing.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### changedRequest (409) ```json { "error": { "code": "conflict", "message": "Idempotency-Key was already used with a different request.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 422 Отсутствуют обязательные поля (включая middle_name для person), неверный документ/контрольная сумма, customer_uuid не разрешён, external_id уже занят. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (422) ```json { "error": { "code": "validation_failed", "message": "A valid Idempotency-Key header is required.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### fields (422) ```json { "error": { "code": "validation_failed", "message": "The name field is required.", "details": { "name": [ "The name field is required." ] }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Превышен общий лимит текущего API-ключа по всем методам (в том числе replay). Квота отдельных EPP-команд и фоновых задач не равна этому HTTP-лимиту. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Необработанная ошибка выполнения; это не подтверждение отсутствия побочного эффекта. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции