C# SDK
The Obsidian C# SDK is a single file, Obsidian.cs, that handles TLS pinning, request signing, response verification, and HWID generation for you.
Drop the file into your project, add a NuGet reference to BouncyCastle, and you have a working client. Download the source from your app's SDK Integration tab in the dashboard. See Applications for where to grab it and how to rotate the secret it comes baked with.
Namespace and types
Everything lives in ObsidianSDK. The public surface is small.
namespace ObsidianSDK
{
public class ObsidianException : Exception { }
public class Obsidian
{
public string Session { get; }
public string Hwid { get; }
public bool Capture { get; set; }
public Obsidian(string apiKey, string apiSecret, string version);
public static string GetHwid();
public static string CaptureScreenshot();
public Dictionary<string, object> Init();
public Dictionary<string, object> Login(string username, string password);
public Dictionary<string, object> Register(string username, string password, string license);
public Dictionary<string, object> License(string license);
}
}
Every response comes back as a Dictionary<string, object> deserialized from the server JSON. Nested objects (like user or app) are themselves dictionaries, so you cast when you drill in.
Constructor
public Obsidian(string apiKey, string apiSecret, string version)
All three parameters are required and throw ArgumentException on null or empty. apiKey and apiSecret come from the dashboard. version must match the version configured on the app, otherwise the server returns a mismatch on Init.
See Applications for how version enforcement behaves when force_update is on.
The constructor computes the HWID synchronously through WMI plus a registry read, stores it on the Hwid property, and sets Capture to true.
The static constructor installs a global TLS handler on ServicePointManager that pins the SPKI SHA-256 of obsidianauth.com. Any other HttpWebRequest in your process that hits that host will also be pinned. Requests to other hosts are passed through unchanged.
Properties
Session is populated by Init() and sent as the session field on every subsequent call. Hwid is the 32-character lowercase hex fingerprint the SDK derives from your machine; both properties have private setters. Capture is the one knob you can flip: when true, Login, Register, and License include a base64 PNG screenshot; set it to false for headless or console builds.
See HWID Lock for how the server uses the HWID and when a mismatch triggers a 403.
Methods
Init
Dictionary<string, object> Init();
Calls POST /api/client/init with { api_key, version }. On success the returned dictionary carries:
success(bool)session(string), which is also written to theSessionpropertyapp(dictionary) withname,version, andhwid_lock
Login, Register, and License call Init() automatically when Session is null, so calling it yourself up front is optional. Do it if you want to read the app block or fail fast at startup.
Register
Dictionary<string, object> Register(string username, string password, string license);
Creates an end user bound to a license key. The payload contains session, username, password, license, hwid, and screenshot.
After a successful register the SDK immediately calls Login(username, password) so last_login populates and downstream calls have a live session. Login failures inside this second call are swallowed; the register result is still returned. See the Changelog for the v1.1 behavior change.
On success the response has success, message, and user { username, level, expires_at }.
Login
Dictionary<string, object> Login(string username, string password);
Sends session, username, password, hwid, and screenshot. On success you get success, message, and user { username, level, expires_at }.
License
Dictionary<string, object> License(string license);
License-only authentication with no username or password. Payload: session, license, hwid, screenshot. On success: success, message, level, expires_at.
GetHwid (static)
public static string GetHwid();
Returns the same 32-character hex fingerprint the SDK uses internally. It reads, in order: HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid, Win32_ComputerSystemProduct.UUID, Win32_Processor.ProcessorId, and Win32_DiskDrive.SerialNumber. If every source fails, the fallback is obsidian-nohw-<hostname> hashed to the same 32 chars.
CaptureScreenshot (static)
public static string CaptureScreenshot();
Returns a base64 PNG of the virtual screen or an empty string on any failure. Dimensions come from GetSystemMetrics(78, 79), falling back to 1920x1080. Internally it uses System.Drawing.Bitmap with Graphics.CopyFromScreen.
Exceptions
Every failure path throws ObsidianException. The constructor is the one exception: null or empty inputs throw ArgumentException instead.
| Message | When |
|---|---|
network write: {inner} | Failed writing the request body. |
network: {inner} | Failed reading the response and no HTTP response came back. |
unsigned server response (refusing to trust) | Response missing the X-Response-Signature header. |
unknown server signing key '{kid}' (SDK expected 'k1') | Server sent an X-Response-Kid the SDK does not recognize. |
invalid server signature (bad base64) | Signature header was not valid base64. |
invalid server signature (response forged or tampered) | Ed25519 verification failed. |
malformed server response | Body was not parseable JSON. |
Server detail string | Any non-2xx response. Passed through verbatim from the detail field. |
Server error strings
These are the exact detail values raised by /api/client/* that surface as ObsidianException.Message. Match on them if you need branch-specific UX; otherwise show the message.
| Message | Endpoint | HTTP |
|---|---|---|
Application disabled | init, register, login, license | 403 |
Object detail with message ("Outdated client version. Update required."), latest_version, update_url, force_update (see note below) | init | 426 |
Too many failed attempts, try again later. | register, login | 429 |
License validation is temporarily paused for this application. | register, license | 403 |
Access is temporarily paused for this application. | login | 403 |
Invalid license key | register, license | 400 |
License key is banned | register, license | 403 |
License key already used | register | 400 |
Username already taken | register | 400 |
Heartbeat session required. Call /heartbeat/start first. | login, license | 403 |
License activation in progress, retry. | license | 409 |
License not valid | license | 403 |
HWID mismatch. Key locked to another device. | license | 403 |
License expired | license | 403 |
License state changed mid-activation, please retry | register | 409 |
Invalid username or password | login | 401 |
The 426 row is special: the server sends detail as a JSON object, not a string. The SDK's error path calls Dv.ToString() on the deserialized value, and for a Dictionary<string, object> that yields the .NET type-name string (System.Collections.Generic.Dictionary2[System.String,System.Object]), not the human-readable message. To read message, latest_version, update_url, and force_update, catch the exception and re-issue the request against /api/client/init` yourself, then inspect the raw response.
See Client Endpoints for the full request/response contract.
Framework and runtime requirements
The SDK targets classic .NET Framework, not .NET Core or .NET 5+.
| Requirement | Notes |
|---|---|
| Target framework | .NET Framework 4.7.2 or newer. .NET Core, .NET 5, and .NET 6/7/8 are not supported because the SDK uses System.Web.Script.Serialization.JavaScriptSerializer, which was intentionally not ported to modern .NET. |
| Platform | Windows only. System.Management, Microsoft.Win32.Registry, and System.Drawing are Windows-specific. |
| CPU | AnyCPU is fine. HWID probes work on both x86 and x64 Windows. |
| Dependency | BouncyCastle, used for Ed25519 verification and X.509 SPKI extraction. |
| BCL references | System.Web.Extensions (for JavaScriptSerializer), System.Management, System.Drawing. |
The static constructor forces Tls12 | Tls11 | Tls on ServicePointManager and installs the SPKI-pinning validation callback. Because that callback is global, any other request in your process to obsidianauth.com will also be pinned. Other hosts are passed through.
csproj snippet
A minimal .NET Framework project referencing the SDK and its dependencies:
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<OutputType>Exe</OutputType>
<RootNamespace>MyApp</RootNamespace>
<AssemblyName>MyApp</AssemblyName>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Drawing" />
<Reference Include="System.Management" />
<Reference Include="System.Web.Extensions" />
<Reference Include="BouncyCastle.Crypto">
<HintPath>packages\BouncyCastle.1.8.9\lib\BouncyCastle.Crypto.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="Obsidian.cs" />
<Compile Include="Program.cs" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
Install BouncyCastle:
nuget install BouncyCastle -Version 1.8.9 -OutputDirectory packages
Minimal usage
A typical username-and-password flow:
using System;
using System.Collections.Generic;
using ObsidianSDK;
class Program
{
static void Main()
{
var Obs = new Obsidian(
apiKey: "ak_your_key",
apiSecret: "sk_your_secret",
version: "1.0.0");
try
{
Obs.Init();
var Result = Obs.Login("alice", "hunter2");
var User = (Dictionary<string, object>)Result["user"];
Console.WriteLine("logged in as " + User["username"]);
}
catch (ObsidianException Ex)
{
Console.Error.WriteLine("auth failed: " + Ex.Message);
Environment.Exit(1);
}
}
}
For a console-only or headless build, turn the screenshot off before calling any auth method:
var Obs = new Obsidian("ak_your_key", "sk_your_secret", "1.0.0");
Obs.Capture = false;
Obs.License("XXXX-YYYY-ZZZZ-WWWW");
What the C# SDK does not do
There is no heartbeat client. If the app has require_heartbeat on, Login and License will 403 with Heartbeat session required. Call /heartbeat/start first. Use the C++ SDK for products that need continuous session liveness.
There is no verify_proof per-call receipt. Response integrity is enforced inside Post via the Ed25519 signature check on X-Response-Signature; there is no separate proof object exposed to callers.
There is no async API and no auto-retry. Every call blocks, and network faults throw on the first failure. Wrap in Task.Run yourself if you need to keep a UI thread responsive.