CrmSolid 0.3.0-alpha.1

This is a prerelease version of CrmSolid.
dotnet add package CrmSolid --version 0.3.0-alpha.1
                    
NuGet\Install-Package CrmSolid -Version 0.3.0-alpha.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="CrmSolid" Version="0.3.0-alpha.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="CrmSolid" Version="0.3.0-alpha.1" />
                    
Directory.Packages.props
<PackageReference Include="CrmSolid" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add CrmSolid --version 0.3.0-alpha.1
                    
#r "nuget: CrmSolid, 0.3.0-alpha.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package CrmSolid@0.3.0-alpha.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=CrmSolid&version=0.3.0-alpha.1&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=CrmSolid&version=0.3.0-alpha.1&prerelease
                    
Install as a Cake Tool

CrmSolid .NET SDK

NuGet NuGet downloads CI License: MIT Targets

The official .NET SDK for the CRM Solid omnichannel AI CRM platform. It is strongly typed, scope-aware, and safe-write by design: it exposes the full public API surface — CRM, messaging, sales, finance, automation and developer tooling — while the operations the platform intentionally blocks (hard deletes, unscoped writes) stay out of reach.

  • Typed clients for the public REST API v1 (/v1/*) over a shared HttpClient
  • Bearer auth (csk_<env>_<key>) for /v1/* + /mcp, and HMAC for legacy /public/*
  • Cursor pagination as IAsyncEnumerable<T> (StreamAllAsync)
  • Automatic 429 retry honoring Retry-After / X-RateLimit-Reset
  • Webhook signature verification (WebhookSignature.Verify)
  • MCP JSON-RPC client wrapper (POST /mcp)
  • IHttpClientFactory / DI friendly · multi-target netstandard2.0, net8.0, net9.0

Install

dotnet add package CrmSolid

Create an API key from your Developer settings. Pick only the scopes your integration needs — the create-key dialog groups them, and the powerful ones (finance:write, email:send, keys:manage) are unchecked by default. The token is shown once; store it securely. Format: csk_<env>_<12-char-keyId><32-char-secret>.


Quickstart

using CrmSolid;
using CrmSolid.Models;

var client = new CrmSolidClient("csk_live_xxxxxxxxxxxxYourSecretHere...");

// Who am I? (smoke test — returns identity + granted scopes)
var me = await client.Me.GetAsync();
Console.WriteLine($"{me.Email} — scopes: {string.Join(", ", me.ApiKey!.Scopes)}");

// List the first 50 Telegram contacts
var page = await client.Contacts.ListAsync(limit: 50, platform: Platform.Telegram);
foreach (var c in page.Items)
    Console.WriteLine($"#{c.Id} {c.Name} @{c.Username}");

// Stream every contact across all pages (cursor pagination, fetched on demand)
await foreach (var c in client.Contacts.StreamAllAsync(pageSize: 100))
    Console.WriteLine(c.Name);

Authentication

Bearer (default — /v1/* and /mcp)

var client = new CrmSolidClient("csk_live_...");
// or, with options:
var client = new CrmSolidClient(new CrmSolidOptions
{
    ApiKey  = "csk_live_...",
    Timeout = TimeSpan.FromSeconds(30),
    MaxRetries = 3,                 // 429 auto-retry attempts
    AutoRetryOnRateLimit = true,
});

Each key carries a fixed set of scopes. A call that needs a scope your key lacks throws CrmSolidForbiddenException. Use the constants in CrmSolid.Models.Scopes instead of magic strings.

Dependency injection

// Program.cs
services.AddCrmSolid(opt => opt.ApiKey = builder.Configuration["CrmSolid:ApiKey"]);

public class MyService(CrmSolidClient crm)
{
    public Task<User> WhoAsync() => crm.Me.GetAsync();
}

AddCrmSolid wires the client through IHttpClientFactory, so handler lifetimes and connection pooling are managed for you.

HMAC (legacy /public/*)

var client = new CrmSolidClient(new CrmSolidOptions
{
    HmacKeyId  = "abc123def456",
    HmacSecret = "your-secret",
});

The SDK signs each request HMAC-SHA256(secret, METHOD\npath+query\nsha256-hex(body)\nunix-ts) and sets X-API-Key / X-Timestamp / X-Signature.


Capability matrix

Every method is scope-gated. R = read, W = write (safe-write), ★ = powerful (off by default).

Resource Key methods Scope
client.Me GetAsync none R
client.Contacts ListAsync · GetAsync · CreateAsync · StreamAllAsync contacts:read R
AddTagAsync · RemoveTagAsync · SetLeadScoreAsync · AssignAsync · AddActivityAsync · SetStageAsync contacts:write W
client.Conversations ListRecentAsync · GetThreadAsync · StreamThreadAsync contacts:read R
client.Accounts ListAsync contacts:read R
client.TelegramMessages SendAsync telegram:send W
GetAsync telegram:read R
client.TwitterMessages SendAsync twitter:send W
SearchAsync contacts:read R
client.Sequences ListAsync · GetAsync sequences:read R
PauseAsync · ResumeAsync sequences:write W
client.Deals ListAsync · GetAsync · StreamAllAsync deals:read R
CreateAsync · ChangeStageAsync deals:write W
CloseAsync (books revenue) deals:write + finance:write ★
client.Tasks ListAsync · GetAsync tasks:read R
CreateAsync · ChangeStatusAsync · CompleteAsync tasks:write W
client.Pipelines ListAsync · GetAsync pipelines:read R
client.Finance GetSummaryAsync · ListTransactionsAsync · ListInvoicesAsync · ListRevenueSourcesAsync finance:read R
CreateTransactionAsync · MarkInvoicePaidAsync finance:write ★
client.Email ListThreadsAsync · GetThreadAsync · StreamThreadsAsync email:read R
SetStatusAsync · AssignAsync email:write W
ReplyAsync (sends mail) email:send ★
client.Analytics GetSummaryAsync · GetMessagingStatsAsync · ListTopContactsAsync analytics:read R
client.AiAgents ListAsync · GetAsync agents:read R
TestAsync (dry-run, never sends) agents:run W
client.Jobs ListAsync · GetAsync · StreamAllAsync jobs:read R
client.Webhooks ListAsync · GetAsync · ListDeliveriesAsync · ListEventTypesAsync webhooks:read R
CreateAsync · UpdateAsync · RotateSecretAsync · SendTestAsync · DeleteAsync webhooks:write W
client.ApiKeys ListAsync · CreateAsync (attenuated) · RevokeAsync keys:manage ★
client.Mcp ListToolsAsync · CallToolAsync · SendAsync key-dependent —

Full OpenAPI spec: https://app.crmsolid.com/openapi/public-v1.yaml.


Scopes

Constants live in CrmSolid.Models.Scopes. Default-granted scopes cover the standard surface; the three powerful ones are off unless requested explicitly.

Scope Grants Default
contacts:read / contacts:write Contacts, tags, lead score, assignment, activity, stage, conversations, accounts ✅
telegram:send / telegram:read Queue Telegram messages / read job status ✅
twitter:send Send Twitter (X) DMs ✅
sequences:read / sequences:write List sequences / pause-resume ✅
analytics:read Dashboard KPIs, messaging stats, top contacts ✅
deals:read / deals:write Pipeline read / create + move stages ✅
tasks:read / tasks:write Tasks read / create + status ✅
email:read / email:write Inbox read / status + assignment ✅
finance:read Summary, transactions, invoices, revenue sources ✅
pipelines:read Pipeline boards + stages ✅
webhooks:read / webhooks:write Endpoints + deliveries / register + rotate ✅
agents:read / agents:run List agents / run the test playground (no send) ✅
jobs:read Outbound message-job monitor ✅
finance:write ★ Create transactions, mark invoices paid —
email:send ★ Send a reply inside a thread —
keys:manage ★ Mint / revoke API keys (attenuated to your scopes) —

Examples by resource

// ── Twitter / X DMs ────────────────────────────────────────────────
var accounts = await client.Accounts.ListAsync(type: "twitter");
await client.TwitterMessages.SendAsync(new SendDmRequest
{
    XAccountId = accounts.Items[0].Id, ContactId = 42, Text = "Thanks for the follow!",
});

// ── Sequences (campaigns) ──────────────────────────────────────────
foreach (var s in await client.Sequences.ListAsync(status: "active"))
    Console.WriteLine($"{s.Name}: {s.ProcessedTargets}/{s.TotalTargets}");
await client.Sequences.PauseAsync(sequenceId: 3);   // idempotent

// ── Analytics ──────────────────────────────────────────────────────
var stats = await client.Analytics.GetMessagingStatsAsync(windowDays: 7);
Console.WriteLine($"7-day success rate: {stats.SuccessRate}%");

// ── Conversations ──────────────────────────────────────────────────
await foreach (var msg in client.Conversations.StreamThreadAsync(contactId: 42))
    Console.WriteLine($"[{msg.Direction}] {msg.Text}");

// ── Pipelines ──────────────────────────────────────────────────────
foreach (var board in await client.Pipelines.ListAsync())
    Console.WriteLine($"{board.Name}: {board.Stages.Count} stages, {board.ContactCount} contacts");
await client.Contacts.SetStageAsync(contactId: 42, stage: "negotiation");

// ── AI Agents (dry-run, never sends) ───────────────────────────────
var preview = await client.AiAgents.TestAsync(agentId: 1,
    new TestAgentRequest { IncomingText = "Do you offer annual billing?" });
Console.WriteLine(preview.Output);

// ── Jobs monitor ───────────────────────────────────────────────────
await foreach (var job in client.Jobs.StreamAllAsync(status: "failed"))
    Console.WriteLine($"#{job.Id} {job.Status}: {job.LastError}");

// ── Finance (write — needs finance:write) ──────────────────────────
await client.Finance.CreateTransactionAsync(new CreateTransactionRequest
{
    Type = "income", Amount = 1499.00m, Currency = "USD", Description = "Annual plan",
});
await client.Finance.MarkInvoicePaidAsync(invoiceId: 87);

// ── Email reply (needs email:send) ─────────────────────────────────
await client.Email.ReplyAsync(threadId: 12, new EmailReplyRequest
{
    BodyText = "Happy to help — here are the details…",   // To defaults to the last inbound sender
});

// ── Close a deal as won (needs deals:write + finance:write) ────────
await client.Deals.CloseAsync(dealId: 9);   // books an income transaction for the deal value

Webhooks + signature verification

// Register an endpoint — the signing secret is returned ONCE
var created = await client.Webhooks.CreateAsync(new CreateWebhookRequest
{
    Url = "https://example.com/crmsolid/hook",
    EventTypes = new[] { "contact.created", "deal.stage_changed" },  // or null for all
});
string secret = created.Secret;   // store this now

// In your receiver (ASP.NET Core Minimal API), verify the raw body before trusting it:
app.MapPost("/crmsolid/hook", async (HttpRequest req) =>
{
    using var reader = new StreamReader(req.Body);
    var raw = await reader.ReadToEndAsync();
    var sig = req.Headers["X-Webhook-Signature"].ToString();

    if (!CrmSolid.Webhooks.WebhookSignature.Verify(secret, raw, sig))
        return Results.Unauthorized();

    // ... process the verified payload ...
    return Results.Ok();
});

Self-service API keys (attenuated)

// A key with keys:manage can mint child keys — but only with scopes it already holds.
var ci = await client.ApiKeys.CreateAsync(new CreateApiKeyRequest
{
    Name = "CI pipeline", Scopes = "contacts:read deals:read", EnvLabel = "test",
});
Console.WriteLine(ci.Token);   // csk_test_… — shown once
await client.ApiKeys.RevokeAsync(ci.Id);

Pagination & streaming

List endpoints are cursor-paginated and return { Items, NextCursor, HasMore }. Pass NextCursor as after to advance, or let StreamAllAsync walk every page for you:

var first = await client.Deals.ListAsync(limit: 50);
var next  = first.HasMore ? await client.Deals.ListAsync(after: first.NextCursor) : null;

await foreach (var deal in client.Deals.StreamAllAsync(stage: "proposal"))
    Console.WriteLine(deal.Title);

Error handling

Non-2xx responses raise typed exceptions, all derived from CrmSolidApiException:

Status Exception Meaning
400 CrmSolidValidationException Body / params failed validation
401 CrmSolidAuthException Missing / invalid bearer
403 CrmSolidForbiddenException Scope not granted, or IP not allowed
404 CrmSolidNotFoundException Absent or not owned by caller
429 CrmSolidRateLimitException Per-key limit hit (auto-retried first)
5xx CrmSolidApiException Server-side — quote RequestId to support

All expose StatusCode, the parsed ApiError envelope, and the X-Request-Id header.

try
{
    await client.Deals.CloseAsync(9);
}
catch (CrmSolidForbiddenException ex)
{
    // e.g. key has deals:write but not finance:write
    Console.WriteLine(ex.Error?.Message);
}

Rate limiting

The API limits per key and reports X-RateLimit-Limit / -Remaining / -Reset. On HTTP 429 the SDK retries automatically up to MaxRetries (default 3), honoring Retry-After then X-RateLimit-Reset. When retries are exhausted, CrmSolidRateLimitException is thrown with RetryAfter, Limit, Remaining, and ResetAtUnix. Disable with new CrmSolidOptions { AutoRetryOnRateLimit = false }.


Safety & boundaries

The SDK mirrors the platform's intentional limits, so an integration cannot do damage:

  • No hard deletes. Removals are soft (archive / revoke) or absent.
  • Idempotent safe-writes. Tag/untag, pause/resume, mark-paid and close are safe to retry.
  • Powerful scopes are opt-in. finance:write, email:send, keys:manage are never default; deals:close additionally requires finance:write because it books revenue.
  • Attenuated key minting. A minted key can only carry scopes the parent holds, never *.
  • AI agent runs are dry-runs. AiAgents.TestAsync returns the would-be reply but never sends it.
  • Email is reply-only within an existing thread — there is no bulk/compose blast.

Versioning

Follows Semantic Versioning. Pre-1.0 releases may break between minors — pin the patch. First stable target: 1.0.0. See CHANGELOG.md.

Contributing

git clone https://github.com/CRM-Solid/crmsolid-dotnet.git
cd crmsolid-dotnet
dotnet test

License

MIT © 2026 CRM Solid

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.3.0-alpha.1 79 6/6/2026