CrmSolid 0.3.0-alpha.1
dotnet add package CrmSolid --version 0.3.0-alpha.1
NuGet\Install-Package CrmSolid -Version 0.3.0-alpha.1
<PackageReference Include="CrmSolid" Version="0.3.0-alpha.1" />
<PackageVersion Include="CrmSolid" Version="0.3.0-alpha.1" />
<PackageReference Include="CrmSolid" />
paket add CrmSolid --version 0.3.0-alpha.1
#r "nuget: CrmSolid, 0.3.0-alpha.1"
#:package CrmSolid@0.3.0-alpha.1
#addin nuget:?package=CrmSolid&version=0.3.0-alpha.1&prerelease
#tool nuget:?package=CrmSolid&version=0.3.0-alpha.1&prerelease
CrmSolid .NET SDK
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 sharedHttpClient - 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-targetnetstandard2.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:manageare never default;deals:closeadditionally requiresfinance:writebecause it books revenue. - Attenuated key minting. A minted key can only carry scopes the parent holds, never
*. - AI agent runs are dry-runs.
AiAgents.TestAsyncreturns 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 | Versions 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. |
-
.NETStandard 2.0
- Microsoft.Bcl.AsyncInterfaces (>= 8.0.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.2)
- Microsoft.Extensions.Http (>= 8.0.1)
- Microsoft.Extensions.Options (>= 8.0.2)
- System.Text.Json (>= 8.0.5)
- System.Threading.Tasks.Extensions (>= 4.5.4)
-
net8.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.2)
- Microsoft.Extensions.Http (>= 8.0.1)
- Microsoft.Extensions.Options (>= 8.0.2)
- System.Text.Json (>= 8.0.5)
-
net9.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.2)
- Microsoft.Extensions.Http (>= 8.0.1)
- Microsoft.Extensions.Options (>= 8.0.2)
- System.Text.Json (>= 8.0.5)
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 |