Skip to content

C#/.NET SDK

Zero-dependency C#/.NET SDK for the Synapse API. Published as PyrxSynapse on NuGet. Uses only System.Net.Http.HttpClient (no external dependencies).

Requires .NET 6+.


Installation

.NET CLI

bash
dotnet add package PyrxSynapse

Package Manager

powershell
Install-Package PyrxSynapse

PackageReference

xml
<PackageReference Include="PyrxSynapse" Version="0.1.0" />

Quick Start

csharp
using PyrxSynapse;
using PyrxSynapse.Models;
 
var client = new SynapseClient(new SynapseConfig
{
ApiKey = "psk_live_your_api_key",
WorkspaceId = "your_workspace_id"
});
 
// Track an event
var result = await client.TrackAsync(new TrackParams
{
ExternalId = "user_123",
EventName = "purchase_completed",
Attributes = new Dictionary<string, object>
{
["order_id"] = "ord_456",
["amount"] = 99.99,
["currency"] = "USD"
}
});
Console.WriteLine(result.EventId); // "evt_8f14e45f-..."
 
// Identify a contact
var contact = await client.IdentifyAsync(new IdentifyParams
{
ExternalId = "user_123",
Email = "[email protected]",
FirstName = "Jane",
LastName = "Doe",
Properties = new Dictionary<string, object> { ["plan"] = "pro", ["signup_source"] = "website" },
Tags = new List<string> { "paying", "beta-tester" }
});
Console.WriteLine(contact.Email); // "[email protected]"
 
// Send a transactional email
var send = await client.SendAsync(new SendParams
{
TemplateSlug = "order-confirmation",
To = new Dictionary<string, object>
{
["user_id"] = "user_123",
["email"] = "[email protected]",
["first_name"] = "Jane"
},
Attributes = new Dictionary<string, object> { ["order_id"] = "ord_456" }
});
Console.WriteLine(send.Status); // "sent"
Tip

Get your API key and workspace ID from the dashboard at Settings > API Keys.


Configuration

csharp
var client = new SynapseClient(new SynapseConfig
{
ApiKey = "psk_live_xxx", // Required. API key from workspace settings.
WorkspaceId = "ws_xxx", // Required. Your workspace ID.
BaseUrl = "https://...", // Default: https://synapse-api.pyrx.tech
Timeout = TimeSpan.FromSeconds(30), // Default: 30 seconds
MaxRetries = 3 // Default: 3. Set to 0 to disable retries.
});
ParameterTypeDefaultDescription
ApiKeystringrequiredYour Synapse API key (psk_live_* or psk_test_*)
WorkspaceIdstringrequiredYour workspace identifier
BaseUrlstringhttps://synapse-api.pyrx.techAPI base URL
TimeoutTimeSpan30 secondsRequest timeout
MaxRetriesint3Retry count for 429/5xx errors. Set to 0 to disable.

Environment detection: The SDK detects test or live from your API key prefix (psk_test_* vs psk_live_*), available via client.Environment.

Retry behavior: The SDK automatically retries on 429, 500, 502, 503, and 504 with exponential backoff and jitter (capped at 30s). On 429, uses the Retry-After header when present. Network errors (HttpRequestException, TaskCanceledException) are also retried. Client errors (400, 401, 403, 404, 422) are never retried.

IDisposable: SynapseClient implements IDisposable, so you can use it with using statements or dependency injection scopes.


Track Events

Single Event

csharp
var result = await client.TrackAsync(new TrackParams
{
ExternalId = "user_123",
EventName = "purchase_completed",
Attributes = new Dictionary<string, object>
{
["order_id"] = "ord_456",
["amount"] = 99.99,
["currency"] = "USD"
},
Contact = new Dictionary<string, object>
{
["email"] = "[email protected]",
["first_name"] = "Jane"
},
IdempotencyKey = "purchase_ord_456", // optional, prevents duplicate processing
OccurredAt = "2026-04-29T10:30:00Z" // optional, defaults to now
});
 
Console.WriteLine(result.EventId); // "evt_8f14e45f-..."
Console.WriteLine(result.Status); // "accepted"
ParameterTypeRequiredDescription
ExternalIdstringYesYour unique user identifier
EventNamestringYesEvent name (e.g., purchase_completed)
AttributesDictionaryNoArbitrary key-value event data
ContactDictionaryNoContact fields to upsert alongside the event
IdempotencyKeystringNoPrevents duplicate processing (7-day TTL)
OccurredAtstringNoISO 8601 timestamp. Defaults to server time.

Batch Events

Track up to 50 events in a single request.

csharp
var result = await client.TrackBatchAsync(new TrackBatchParams
{
Events = new List<TrackParams>
{
new() { ExternalId = "user_1", EventName = "page_view",
Attributes = new() { ["page"] = "/pricing" } },
new() { ExternalId = "user_2", EventName = "page_view",
Attributes = new() { ["page"] = "/docs" } },
new() { ExternalId = "user_1", EventName = "button_clicked",
Attributes = new() { ["button"] = "upgrade" } }
}
});
 
Console.WriteLine(result.Accepted); // 3
Console.WriteLine(result.Rejected); // 0

Identify Contacts

Single Contact

Create or update (upsert) a contact by ExternalId.

csharp
var contact = await client.IdentifyAsync(new IdentifyParams
{
ExternalId = "user_123",
Email = "[email protected]",
FirstName = "Jane",
LastName = "Doe",
Phone = "+1234567890",
Timezone = "America/New_York",
Locale = "en-US",
Properties = new Dictionary<string, object> { ["plan"] = "pro", ["signup_source"] = "website" },
Tags = new List<string> { "paying", "beta-tester" }
});
 
Console.WriteLine(contact.Id); // UUID
Console.WriteLine(contact.ExternalId); // "user_123"
Console.WriteLine(contact.Email); // "[email protected]"

Batch Identify

Upsert up to 1,000 contacts in a single request.

csharp
var result = await client.IdentifyBatchAsync(new IdentifyBatchParams
{
Contacts = new List<IdentifyParams>
{
new() { ExternalId = "user_1", Email = "[email protected]", FirstName = "Alice" },
new() { ExternalId = "user_2", Email = "[email protected]", FirstName = "Bob" }
},
OnConflict = "merge" // "merge" | "skip" | "replace"
});
 
Console.WriteLine(result.Total); // 2
Console.WriteLine(result.Created); // 1
Console.WriteLine(result.Updated); // 1

Send Transactional Email

Send a one-off email using an NLT template, without a flow.

csharp
var result = await client.SendAsync(new SendParams
{
TemplateSlug = "otp-verification",
To = new Dictionary<string, object>
{
["user_id"] = "user_123",
["email"] = "[email protected]",
["first_name"] = "Jane"
},
Attributes = new Dictionary<string, object>
{
["otp_code"] = "847293",
["expiry_minutes"] = 10
},
IdempotencyKey = $"otp_user_123_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}"
});
 
Console.WriteLine(result.Status); // "sent" or "suppressed"
Console.WriteLine(result.EmailLogId); // "el_8f14e45f-..."
Note

Requires a data-scoped API key (or higher). The template must exist in your workspace.


Contact Management

The client.Contacts sub-client provides full CRUD operations. Requires a management or full scoped API key.

List Contacts

csharp
var result = await client.Contacts.ListAsync(new ContactListParams
{
Search = "jane",
Page = 1,
PerPage = 25,
SortBy = "created_at",
SortOrder = "desc"
});
 
Console.WriteLine(result.Meta.Total); // 142
Console.WriteLine(result.Meta.TotalPages); // 6
 
foreach (var c in result.Data)
{
Console.WriteLine($"{c.Email} {c.FirstName}");
}

Get a Contact

csharp
var contact = await client.Contacts.GetAsync("contact_uuid");

Update a Contact

csharp
var updated = await client.Contacts.UpdateAsync("user_123", new ContactUpdateParams
{
Email = "[email protected]",
AddTags = new List<string> { "vip" },
RemoveTags = new List<string> { "trial" }
});

Delete a Contact

csharp
await client.Contacts.DeleteAsync("user_123");

Template Management

The client.Templates sub-client manages email templates. Requires a management or full scoped API key.

List Templates

csharp
var templates = await client.Templates.ListAsync();

Get a Template

csharp
var template = await client.Templates.GetAsync("welcome-email");

Create a Template

csharp
var created = await client.Templates.CreateAsync(new TemplateCreateParams
{
Name = "Welcome Email",
Slug = "welcome-email",
Subject = "Welcome, [first name of contact]!",
BodyHtml = "<h1>Welcome!</h1><p>Thanks for joining.</p>",
SenderName = "PYRX Team",
FromEmail = "[email protected]"
});

Update a Template

csharp
var updated = await client.Templates.UpdateAsync("welcome-email", new TemplateUpdateParams
{
Subject = "Welcome aboard, [first name of contact]!"
});

Preview with Sample Data

csharp
var preview = await client.Templates.PreviewAsync("welcome-email", new TemplatePreviewParams
{
Contact = new Dictionary<string, object> { ["first_name"] = "Jane", ["email"] = "[email protected]" },
TriggerEvent = new Dictionary<string, object> { ["order_id"] = "ord_123" }
});
 
Console.WriteLine(preview.Subject); // Rendered subject
Console.WriteLine(preview.Html); // Rendered HTML
Console.WriteLine(preview.Suppressed); // false
Console.WriteLine(preview.SuppressedReason); // null

Delete a Template

csharp
await client.Templates.DeleteAsync("old-template");

Webhook Verification

Verify incoming webhook signatures to ensure requests are authentically from Synapse. This is a static method on the Webhooks class -- no client instance needed.

csharp
using PyrxSynapse;
using Microsoft.AspNetCore.Mvc;
 
[ApiController]
[Route("webhooks/synapse")]
public class WebhookController : ControllerBase
{
[HttpPost]
public IActionResult HandleWebhook()
{
using var reader = new StreamReader(Request.Body);
var payload = reader.ReadToEndAsync().Result;
 
var headers = new Dictionary<string, string>
{
["svix-id"] = Request.Headers["svix-id"].ToString(),
["svix-timestamp"] = Request.Headers["svix-timestamp"].ToString(),
["svix-signature"] = Request.Headers["svix-signature"].ToString()
};
 
var secret = Environment.GetEnvironmentVariable("SYNAPSE_WEBHOOK_SECRET");
 
try
{
var webhookEvent = Webhooks.Verify(payload, headers, secret);
Console.WriteLine(webhookEvent["type"]); // e.g. "email.delivered"
return Ok();
}
catch (InvalidOperationException e)
{
// Invalid signature, expired timestamp, or missing headers
Console.WriteLine($"Webhook rejected: {e.Message}");
return Unauthorized();
}
}
}

The verification checks:

  • All three svix-* headers are present
  • The timestamp is within 5 minutes (replay attack protection)
  • The HMAC-SHA256 signature matches (supports multiple signatures for key rotation)

You can pass disableTimestampCheck: true to Webhooks.Verify() to disable the timestamp check (useful for testing only).


Error Handling

The SDK provides typed exception classes for every failure mode. All errors extend SynapseException.

csharp
using PyrxSynapse.Errors;
 
try
{
await client.TrackAsync(new TrackParams
{
ExternalId = "u1",
EventName = "test"
});
}
catch (SynapsePlanLimitException e)
{
Console.WriteLine($"Plan limit: {e.LimitType} ({e.Current}/{e.Maximum})");
Console.WriteLine($"Current plan: {e.Plan}");
}
catch (SynapseRateLimitException e)
{
Console.WriteLine($"Rate limited. Retry after {e.RetryAfter}s");
}
catch (SynapseValidationException e)
{
foreach (var err in e.Errors)
{
Console.WriteLine($"{err.Field}: {err.Message}");
}
}
catch (SynapseAuthException e)
{
Console.WriteLine($"Authentication failed: {e.Message}");
}
catch (SynapseException e)
{
Console.WriteLine($"API error {e.Status}: {e.Message}");
}

Error Types

Error ClassHTTP StatusPropertiesWhen
SynapseExceptionAnyStatus, Message, Code, RequestIdBase type for all API errors
SynapseAuthException401, 403MessageInvalid or expired API key, scope mismatch
SynapseValidationException422Errors with Field + MessageRequest body validation failed
SynapseRateLimitException429RetryAfter (seconds)Rate limit exceeded (auto-retried)
SynapsePlanLimitException403LimitType, Current, Maximum, PlanPlan limit reached

Sync vs Async

Every method is available in both async and sync variants. Prefer async in production code.

csharp
// Async (recommended)
var result = await client.TrackAsync(trackParams);
 
// Sync (blocks the calling thread)
var result = client.Track(trackParams);

The async methods use ConfigureAwait(false) throughout, making them safe to use in library code and ASP.NET contexts.

Sync variants (e.g., client.Track(), client.Contacts.List()) are available for all methods in the method reference table below.


Environment Variables

For production deployments, load credentials from environment variables.

csharp
using PyrxSynapse;
 
var client = new SynapseClient(new SynapseConfig
{
ApiKey = Environment.GetEnvironmentVariable("SYNAPSE_API_KEY")!,
WorkspaceId = Environment.GetEnvironmentVariable("SYNAPSE_WORKSPACE_ID")!
});
bash
export SYNAPSE_API_KEY=psk_live_a1b2c3d4e5f67890abcdef1234567890
export SYNAPSE_WORKSPACE_ID=your_workspace_id

Full Method Reference

MethodDescriptionRequired Scope
client.TrackAsync(params)Track a single eventdata
client.TrackBatchAsync(params)Track up to 50 eventsdata
client.IdentifyAsync(params)Upsert a single contactdata
client.IdentifyBatchAsync(params)Upsert up to 1,000 contactsdata
client.SendAsync(params)Send a transactional emaildata
client.Contacts.ListAsync(params)List contacts with paginationmanagement
client.Contacts.GetAsync(id)Get a single contactmanagement
client.Contacts.UpdateAsync(id, params)Update a contactmanagement
client.Contacts.DeleteAsync(id)Delete a contactmanagement
client.Templates.ListAsync()List all templatesmanagement
client.Templates.GetAsync(slug)Get a template by slugmanagement
client.Templates.CreateAsync(params)Create a templatemanagement
client.Templates.UpdateAsync(slug, params)Update a templatemanagement
client.Templates.PreviewAsync(slug, params)Preview rendered templatemanagement
client.Templates.DeleteAsync(slug)Delete a templatemanagement
Webhooks.Verify(payload, headers, secret)Verify webhook signature--

Framework Examples

ASP.NET Core (Minimal API)

csharp
// Program.cs
using PyrxSynapse;
using PyrxSynapse.Models;
 
var builder = WebApplication.CreateBuilder(args);
 
builder.Services.AddSingleton(new SynapseClient(new SynapseConfig
{
ApiKey = Environment.GetEnvironmentVariable("SYNAPSE_API_KEY")!,
WorkspaceId = Environment.GetEnvironmentVariable("SYNAPSE_WORKSPACE_ID")!
}));
 
var app = builder.Build();
 
app.MapPost("/signup", async (SignupRequest req, SynapseClient synapse) =>
{
// Identify the new user
await synapse.IdentifyAsync(new IdentifyParams
{
ExternalId = req.UserId,
Email = req.Email,
Properties = new Dictionary<string, object> { ["plan"] = req.Plan },
Tags = new List<string> { "new-signup" }
});
 
// Track the signup event (triggers flows)
await synapse.TrackAsync(new TrackParams
{
ExternalId = req.UserId,
EventName = "user_signed_up",
Attributes = new Dictionary<string, object> { ["plan"] = req.Plan, ["source"] = "web" }
});
 
return Results.Ok(new { success = true });
});
 
app.Run();
 
record SignupRequest(string UserId, string Email, string Plan);

ASP.NET Core (Controller)

csharp
using Microsoft.AspNetCore.Mvc;
using PyrxSynapse;
 
[ApiController]
[Route("api/[controller]")]
public class EventsController : ControllerBase
{
private readonly SynapseClient _synapse;
 
public EventsController(SynapseClient synapse)
{
_synapse = synapse;
}
 
[HttpPost("track")]
public async Task<IActionResult> Track([FromBody] TrackRequest req)
{
var result = await _synapse.TrackAsync(new TrackParams
{
ExternalId = req.UserId,
EventName = req.EventName,
Attributes = req.Attributes
});
 
return Ok(new { eventId = result.EventId });
}
}
 
public record TrackRequest(string UserId, string EventName, Dictionary<string, object> Attributes);