Server SDKs by language
Preview server clients for C#, Java, Kotlin/JVM, PHP, Go, Python, Ruby, and Dart, plus the Kiota client in @k-otp/sdk for TypeScript. Install, issue and verify, idempotency, and error handling.
Besides the JavaScript and TypeScript SDK, K-OTP has server clients for other languages. Each one is a small, hand-written client around code generated from the public OpenAPI spec with Kiota. They are open source in the sdks/ directory of github.com/k-otp/sdk.
The language SDKs are a preview at version 0.1.0, versioned separately from the npm package.
They are not published to PyPI, NuGet, Maven Central, Packagist, RubyGems, or pub.dev yet:
install them from the server-sdk-v0.1.0 GitHub
Release, and Go from its module
path. Pin the exact version; method names and options may still change before 1.0.
These clients take an sk_ secret key and belong on your server only. For browsers and pk_ keys, use @k-otp/sdk and its framework and UI imports.
Packages
| Language | Package | Version | Install from | Tested with |
|---|---|---|---|---|
| C# | KOtp.Sdk | 0.1.0 | GitHub Release: KOtp.Sdk.0.1.0.nupkg | .NET 8 |
| Java | dev.kotp:kotp-sdk | 0.1.0 | GitHub Release: JAR + POM (Java 17 bytecode) | JDK 21 |
| Kotlin/JVM | dev.kotp:kotp-sdk (Java JAR) | 0.1.0 | Same JAR as Java | Kotlin 2.1.20, JDK 21 |
| PHP | k-otp/sdk | 0.1.0 | GitHub Release: kotp-sdk-php-0.1.0.zip | PHP 8.4 (requires 8.2 or later) |
| Go | github.com/k-otp/sdk/sdks/go | v0.1.0 | Go module proxy | Go 1.26 |
| Python | kotp-sdk (import kotp_sdk) | 0.1.0 | GitHub Release: wheel or sdist | Python 3.13 |
| Ruby | kotp_sdk | 0.1.0 | GitHub Release: kotp_sdk-0.1.0.gem | Ruby 3.3 |
| Dart | kotp_sdk | 0.1.0 | GitHub Release: kotp-sdk-dart-0.1.0.tar.gz | Dart 3.9, server (dart:io) |
| TypeScript | @k-otp/sdk/kiota | npm 1.3.0+ | npm, part of @k-otp/sdk | Node.js 20.19, 22, 24 |
Release assets download from https://github.com/k-otp/sdk/releases/download/server-sdk-v0.1.0/<file>. Check them against the release's SHA256SUMS:
shasum -a 256 -c SHA256SUMS --ignore-missingFor TypeScript on a server, @k-otp/sdk/server remains the typed, idiomatic client. @k-otp/sdk/kiota is the same JSON-in, JSON-out client as the other languages, for teams that want one API shape across services.
What every client does
- Nine operations. Issue, verify, status, issue list, issue detail, credit ledger, balance, template list, and template detail: the operations in the API reference. Method names follow each language's conventions (
issue,IssueAsync,issue_detail, …). - JSON in, JSON out. Inputs use the API's JSON field names (
phoneNumber,purpose,idempotencyKey,issueId,code), and responses are the language's native JSON values. Keep phone numbers and codes as strings so leading zeros survive. - API 1.8.0. The 0.1.0 clients are generated from OpenAPI spec 1.8.0, so issue accepts every current field, including
smsFallback(Channels and SMS fallback). - Secret keys only. The constructor rejects any key that does not start with
sk_. - Locked to the API origin. The base URL defaults to
https://api.k-otp.dev/v1and must be HTTPS (loopback HTTP is allowed for local tests). Redirects and requests to any other origin are rejected, so the key is only ever sent to the configured API. The default timeout is 10 seconds.
Configuration mistakes (a missing or non-sk_ key, an invalid base URL, a missing or invalid idempotency key) fail before any request is sent, with a configuration error instead of an API error: for example ArgumentException in C#, IllegalArgumentException in Java, TypeError in TypeScript and Python, or kotp.ConfigurationError in Go.
Idempotency and retries
issue requires an idempotencyKey in its input: 1 to 128 printable ASCII characters without spaces (surrounding whitespace is trimmed), for example a UUID. The client validates it before sending and sends the same key as the Idempotency-Key header and in the body. Create one key per send attempt, store it, and reuse it when you retry that attempt (Idempotency).
The clients never retry automatically. Two cases need care:
- A timeout or network failure raises a transport error whose
outcomeis"unknown": the code may already have been sent. Retryissuewith the same key; never start over with a new key (Unknown outcomes). - HTTP 503 raises an API error. You can opt in to exactly one automatic retry of
issueafter a503, with the same body and key.verifyis never retried.
| Language | Allow one issue retry after 503 |
|---|---|
| C# | new RequestOptions(Retry503: true) |
| Java/Kotlin | new KotpClient.RequestOptions(Map.of(), null, true) |
| PHP | ['retry503' => true] |
| Go | kotp.RequestOptions{Retry503: true} |
| Python | retry503=True |
| Ruby | retry503: true |
| Dart | options: RequestOptions(retry503: true) |
| TypeScript | { retry503: true } |
Errors
A non-2xx response raises an API error that keeps the whole error envelope: the HTTP status, envelope (defined, code, status, message, data), the X-Request-Id value (include it when you contact support), the wait before retrying in milliseconds (from data.retryAfterMs, else Retry-After; set on 429 and on 503 responses that send one), and the response headers with lowercase names. Branch on the envelope's code.
A wrong or expired code is not an error: verify returns verified: false with a reasonCode (Verification results).
| Language | API error | Transport error (outcome "unknown") |
|---|---|---|
| C# | KotpApiException: Status, Envelope, RequestId, RetryAfterMs | KotpTransportException |
| Java/Kotlin | KotpClient.KotpApiException: status, envelope, requestId, retryAfterMs | KotpClient.KotpTransportException |
| PHP | KOtp\KotpApiError: status, envelope, requestId, retryAfterMs | KOtp\KotpTransportError |
| Go | *kotp.APIError: Status, Envelope, RequestID, RetryAfterMs | *kotp.TransportError |
| Python | KotpApiError: status, envelope, request_id, retry_after_ms | KotpTransportError |
| Ruby | KOtp::ApiError: status, envelope, request_id, retry_after_ms | KOtp::TransportError |
| Dart | KotpApiError: status, envelope, requestId, retryAfterMs | KotpTransportError |
| TypeScript | KotpApiError: status, envelope, requestId, retryAfterMs | KotpTransportError |
C# (.NET)
Download KOtp.Sdk.0.1.0.nupkg into a local ./packages directory, add it as a NuGet source, and reference the package. Keep nuget.org enabled: the package's runtime dependencies come from there.
dotnet nuget add source ./packages --name kotp-local
dotnet add package KOtp.Sdk --version 0.1.0using System.Text.Json.Nodes;
using KOtp;
using var client = new KotpClient(Environment.GetEnvironmentVariable("KOTP_SECRET_KEY")!);
try {
var issued = await client.IssueAsync(new JsonObject {
["phoneNumber"] = "01012345678",
["purpose"] = "login",
["idempotencyKey"] = idempotencyKey, // e.g. Guid.NewGuid().ToString(), stored for retries
});
var issueId = issued!["issueId"]!.GetValue<string>();
// Later, with the code the user entered:
var result = await client.VerifyAsync(new JsonObject { ["issueId"] = issueId, ["code"] = "123456" });
var verified = result!["verified"]!.GetValue<bool>();
} catch (KotpApiException e) {
var code = e.Envelope?["code"]?.GetValue<string>();
Console.Error.WriteLine($"{e.Status} {code} {e.RequestId}");
} catch (KotpTransportException) {
// Outcome unknown: retry IssueAsync with the same idempotencyKey.
}Every method also takes a CancellationToken. The constructor accepts baseUrl and timeoutMs. Dispose the client when you are done.
Java
Download kotp-sdk-0.1.0.jar, kotp-sdk-0.1.0.pom, kotp-sdk-0.1.0-sources.jar, and kotp-sdk-0.1.0-javadoc.jar, install them into your local Maven repository, and add the dependency.
mvn install:install-file -Dfile=kotp-sdk-0.1.0.jar -DpomFile=kotp-sdk-0.1.0.pom \
-Dsources=kotp-sdk-0.1.0-sources.jar -Djavadoc=kotp-sdk-0.1.0-javadoc.jar<dependency>
<groupId>dev.kotp</groupId>
<artifactId>kotp-sdk</artifactId>
<version>0.1.0</version>
</dependency>import com.google.gson.JsonObject;
import dev.kotp.sdk.KotpClient;
try (KotpClient client = new KotpClient(System.getenv("KOTP_SECRET_KEY"))) {
JsonObject issue = new JsonObject();
issue.addProperty("phoneNumber", "01012345678");
issue.addProperty("purpose", "login");
issue.addProperty("idempotencyKey", idempotencyKey); // e.g. UUID.randomUUID().toString()
String issueId = client.issue(issue).getAsJsonObject().get("issueId").getAsString();
// Later, with the code the user entered:
JsonObject verify = new JsonObject();
verify.addProperty("issueId", issueId);
verify.addProperty("code", "123456");
boolean verified = client.verify(verify).getAsJsonObject().get("verified").getAsBoolean();
} catch (KotpClient.KotpApiException e) {
System.err.println(e.status + " " + e.envelope.get("code") + " " + e.requestId);
} catch (KotpClient.KotpTransportException e) {
// Outcome unknown: retry issue with the same idempotencyKey.
}Calls are synchronous and return Gson values. The constructor new KotpClient(apiKey, baseUrl, timeoutMs) overrides the defaults. Close the client to release its connection pool.
Kotlin/JVM
Kotlin/JVM uses the Java JAR and API: install it as in Java. Android and Kotlin Multiplatform are not covered by this release.
import com.google.gson.JsonObject
import dev.kotp.sdk.KotpClient
KotpClient(System.getenv("KOTP_SECRET_KEY")).use { client ->
try {
val issued = client.issue(JsonObject().apply {
addProperty("phoneNumber", "01012345678")
addProperty("purpose", "login")
addProperty("idempotencyKey", idempotencyKey)
})
val issueId = issued.asJsonObject.get("issueId").asString
val result = client.verify(JsonObject().apply {
addProperty("issueId", issueId)
addProperty("code", "123456")
})
val verified = result.asJsonObject.get("verified").asBoolean
} catch (e: KotpClient.KotpApiException) {
println("${e.status} ${e.envelope.get("code")} ${e.requestId}")
} catch (e: KotpClient.KotpTransportException) {
// Outcome unknown: retry issue with the same idempotencyKey.
}
}Calls block, so run them on a suitable dispatcher (for example Dispatchers.IO) from coroutines.
PHP
Download kotp-sdk-php-0.1.0.zip into a local ./packages directory, register it as a Composer artifact repository, and require the package.
composer config repositories.kotp artifact ./packages
composer require k-otp/sdk:0.1.0use KOtp\KotpApiError;
use KOtp\KotpClient;
use KOtp\KotpTransportError;
$client = new KotpClient(getenv('KOTP_SECRET_KEY'));
try {
$issued = $client->issue([
'phoneNumber' => '01012345678',
'purpose' => 'login',
'idempotencyKey' => $idempotencyKey, // e.g. bin2hex(random_bytes(16)), stored for retries
]);
// Later, with the code the user entered:
$result = $client->verify(['issueId' => $issued->issueId, 'code' => '123456']);
$verified = $result->verified;
} catch (KotpApiError $e) {
error_log("{$e->status} {$e->envelope->code} {$e->requestId}");
} catch (KotpTransportError $e) {
// Outcome unknown: retry issue with the same idempotencyKey.
}Calls are synchronous. JSON objects come back as stdClass and lists as arrays. The constructor accepts $baseUrl and $timeoutMs.
Go
go get github.com/k-otp/sdk/sdks/go@v0.1.0import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
kotp "github.com/k-otp/sdk/sdks/go"
)
func sendAndVerify(ctx context.Context, idempotencyKey string) (bool, error) {
client, err := kotp.New(kotp.Options{APIKey: os.Getenv("KOTP_SECRET_KEY")})
if err != nil {
return false, err
}
input, _ := json.Marshal(map[string]any{
"phoneNumber": "01012345678",
"purpose": "login",
"idempotencyKey": idempotencyKey,
})
issued, err := client.Issue(ctx, input, kotp.RequestOptions{})
var apiErr *kotp.APIError
var transportErr *kotp.TransportError
switch {
case errors.As(err, &apiErr):
return false, fmt.Errorf("%d %v %s", apiErr.Status, apiErr.Envelope["code"], apiErr.RequestID)
case errors.As(err, &transportErr):
return false, err // Outcome unknown: retry Issue with the same idempotencyKey.
case err != nil:
return false, err
}
issueID := issued.(map[string]any)["issueId"].(string)
// Later, with the code the user entered:
verifyInput, _ := json.Marshal(map[string]any{"issueId": issueID, "code": "123456"})
result, err := client.Verify(ctx, verifyInput, kotp.RequestOptions{})
if err != nil {
return false, err
}
return result.(map[string]any)["verified"].(bool), nil
}Issue and Verify take the request body as json.RawMessage; responses are decoded JSON (map[string]any for objects). Every call takes a context. Options also has BaseURL and Timeout.
Python
Download the wheel and install it.
python -m pip install ./kotp_sdk-0.1.0-py3-none-any.whlimport asyncio
import os
from kotp_sdk import KotpApiError, KotpClient, KotpTransportError
async def main(idempotency_key: str) -> None:
async with KotpClient(os.environ["KOTP_SECRET_KEY"]) as client:
try:
issued = await client.issue({
"phoneNumber": "01012345678",
"purpose": "login",
"idempotencyKey": idempotency_key, # e.g. str(uuid.uuid4()), stored for retries
})
# Later, with the code the user entered:
result = await client.verify({"issueId": issued["issueId"], "code": "123456"})
verified = result["verified"]
except KotpApiError as e:
print(e.status, e.envelope["code"], e.request_id)
except KotpTransportError:
pass # Outcome unknown: retry issue with the same idempotency_key.
asyncio.run(main(idempotency_key))The client is async. Use async with or call await client.aclose(). The constructor accepts base_url and timeout_ms.
Ruby
Download the gem and install it locally.
gem install --local kotp_sdk-0.1.0.gemrequire 'kotp_sdk'
client = KOtp::Client.new(ENV.fetch('KOTP_SECRET_KEY'))
begin
issued = client.issue({
'phoneNumber' => '01012345678',
'purpose' => 'login',
'idempotencyKey' => idempotency_key # e.g. SecureRandom.uuid, stored for retries
})
# Later, with the code the user entered:
result = client.verify({ 'issueId' => issued['issueId'], 'code' => '123456' })
verified = result['verified']
rescue KOtp::ApiError => e
warn "#{e.status} #{e.envelope['code']} #{e.request_id}"
rescue KOtp::TransportError
# Outcome unknown: retry issue with the same idempotency_key.
endResponses are Hash, Array, and scalar values with string keys. The constructor accepts base_url: and timeout_ms:. Loading the gem installs a guarded process-wide JSON compatibility hook, so keep its locked runtime dependency versions.
Dart
Extract kotp-sdk-dart-0.1.0.tar.gz into a directory and add it as a path dependency named kotp_sdk.
mkdir -p third_party/kotp_sdk
tar -xzf kotp-sdk-dart-0.1.0.tar.gz -C third_party/kotp_sdkdependencies:
kotp_sdk:
path: third_party/kotp_sdkimport 'dart:io';
import 'package:kotp_sdk/kotp_sdk.dart';
final client = KotpClient(Platform.environment['KOTP_SECRET_KEY']!);
try {
final issued = await client.issue({
'phoneNumber': '01012345678',
'purpose': 'login',
'idempotencyKey': idempotencyKey, // stored for retries
}) as Map<String, dynamic>;
// Later, with the code the user entered:
final result = await client.verify({
'issueId': issued['issueId'],
'code': '123456',
}) as Map<String, dynamic>;
final verified = result['verified'] as bool;
} on KotpApiError catch (e) {
stderr.writeln('${e.status} ${e.envelope['code']} ${e.requestId}');
} on KotpTransportError {
// Outcome unknown: retry issue with the same idempotencyKey.
} finally {
client.close();
}The supported runtime is Dart 3.9 on the server (dart:io); like your sk_ key, it does not belong in a Flutter or web app. The constructor accepts baseUrl: and timeoutMs:. Call close() when you are done.
TypeScript (@k-otp/sdk/kiota)
npm i @k-otp/sdkimport { KotpApiError, KotpClient, KotpTransportError } from "@k-otp/sdk/kiota";
const client = new KotpClient({ apiKey: process.env.KOTP_SECRET_KEY! });
try {
const issued = (await client.issue({
phoneNumber: "01012345678",
purpose: "login",
idempotencyKey, // e.g. crypto.randomUUID(), stored for retries
})) as { issueId: string };
// Later, with the code the user entered:
const result = (await client.verify({ issueId: issued.issueId, code: "123456" })) as {
verified: boolean;
reasonCode?: string;
};
} catch (error) {
if (error instanceof KotpApiError) {
console.error(error.status, error.envelope.code, error.requestId);
} else if (error instanceof KotpTransportError) {
// Outcome unknown: retry issue with the same idempotencyKey.
} else {
throw error;
}
}Responses are untyped JSON (unknown); for typed results use @k-otp/sdk/server. The constructor accepts baseUrl, timeoutMs, and a custom fetch. Under the browser export condition, constructing the client throws, so the server client cannot run in a browser bundle.
SDKs
Official SDKs for K-OTP. The JavaScript and TypeScript SDK is one package with imports for browsers, servers, React, Vue, Svelte, and UI components (`npm i @k-otp/sdk`); preview server SDKs cover C#, Java, Kotlin, PHP, Go, Python, Ruby, and Dart.
UI components
Ready-made phone verification forms for React, Vue, and Svelte, headless first with an optional theme. Part of @k-otp/sdk. Install it with `npm i @k-otp/sdk`.