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+ .
bash
dotnet add package PyrxSynapse
powershell
Install-Package PyrxSynapse
xml
<PackageReference Include="PyrxSynapse" Version="0.1.0" />
csharp
1 using PyrxSynapse;
2 using PyrxSynapse.Models;
3
4 var client = new SynapseClient(new SynapseConfig
5 {
6 ApiKey = "psk_live_your_api_key",
7 WorkspaceId = "your_workspace_id"
8 });
9
10 // Track an event
11 var result = await client.TrackAsync(new TrackParams
12 {
13 ExternalId = "user_123",
14 EventName = "purchase_completed",
15 Attributes = new Dictionary<string, object>
16 {
17 ["order_id"] = "ord_456",
18 ["amount"] = 99.99,
19 ["currency"] = "USD"
20 }
21 });
22 Console.WriteLine(result.EventId); // "evt_8f14e45f-..."
23
24 // Identify a contact
25 var contact = await client.IdentifyAsync(new IdentifyParams
26 {
27 ExternalId = "user_123",
29 FirstName = "Jane",
30 LastName = "Doe",
31 Properties = new Dictionary<string, object> { ["plan"] = "pro", ["signup_source"] = "website" },
32 Tags = new List<string> { "paying", "beta-tester" }
33 });
35
36 // Send a transactional email
37 var send = await client.SendAsync(new SendParams
38 {
39 TemplateSlug = "order-confirmation",
40 To = new Dictionary<string, object>
41 {
42 ["user_id"] = "user_123",
44 ["first_name"] = "Jane"
45 },
46 Attributes = new Dictionary<string, object> { ["order_id"] = "ord_456" }
47 });
48 Console.WriteLine(send.Status); // "sent"
csharp
1 var client = new SynapseClient(new SynapseConfig
2 {
3 ApiKey = "psk_live_xxx", // Required. API key from workspace settings.
4 WorkspaceId = "ws_xxx", // Required. Your workspace ID.
5 BaseUrl = "https://...", // Default: https://synapse-api.pyrx.tech
6 Timeout = TimeSpan.FromSeconds(30), // Default: 30 seconds
7 MaxRetries = 3 // Default: 3. Set to 0 to disable retries.
8 });
Parameter Type Default Description ApiKeystring required Your Synapse API key (psk_live_* or psk_test_*) WorkspaceIdstring required Your workspace identifier BaseUrlstring https://synapse-api.pyrx.techAPI base URL TimeoutTimeSpan 30 seconds Request timeout MaxRetriesint 3 Retry 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.
csharp
1 var result = await client.TrackAsync(new TrackParams
2 {
3 ExternalId = "user_123",
4 EventName = "purchase_completed",
5 Attributes = new Dictionary<string, object>
6 {
7 ["order_id"] = "ord_456",
8 ["amount"] = 99.99,
9 ["currency"] = "USD"
10 },
11 Contact = new Dictionary<string, object>
12 {
14 ["first_name"] = "Jane"
15 },
16 IdempotencyKey = "purchase_ord_456", // optional, prevents duplicate processing
17 OccurredAt = "2026-04-29T10:30:00Z" // optional, defaults to now
18 });
19
20 Console.WriteLine(result.EventId); // "evt_8f14e45f-..."
21 Console.WriteLine(result.Status); // "accepted"
Parameter Type Required Description ExternalIdstring Yes Your unique user identifier EventNamestring Yes Event name (e.g., purchase_completed) AttributesDictionary No Arbitrary key-value event data ContactDictionary No Contact fields to upsert alongside the event IdempotencyKeystring No Prevents duplicate processing (7-day TTL) OccurredAtstring No ISO 8601 timestamp. Defaults to server time.
Track up to 50 events in a single request.
csharp
1 var result = await client.TrackBatchAsync(new TrackBatchParams
2 {
3 Events = new List<TrackParams>
4 {
5 new() { ExternalId = "user_1", EventName = "page_view",
6 Attributes = new() { ["page"] = "/pricing" } },
7 new() { ExternalId = "user_2", EventName = "page_view",
8 Attributes = new() { ["page"] = "/docs" } },
9 new() { ExternalId = "user_1", EventName = "button_clicked",
10 Attributes = new() { ["button"] = "upgrade" } }
11 }
12 });
13
14 Console.WriteLine(result.Accepted); // 3
15 Console.WriteLine(result.Rejected); // 0
Create or update (upsert) a contact by ExternalId.
csharp
1 var contact = await client.IdentifyAsync(new IdentifyParams
2 {
3 ExternalId = "user_123",
5 FirstName = "Jane",
6 LastName = "Doe",
7 Phone = "+1234567890",
8 Timezone = "America/New_York",
9 Locale = "en-US",
10 Properties = new Dictionary<string, object> { ["plan"] = "pro", ["signup_source"] = "website" },
11 Tags = new List<string> { "paying", "beta-tester" }
12 });
13
14 Console.WriteLine(contact.Id); // UUID
15 Console.WriteLine(contact.ExternalId); // "user_123"
Upsert up to 1,000 contacts in a single request.
csharp
1 var result = await client.IdentifyBatchAsync(new IdentifyBatchParams
2 {
3 Contacts = new List<IdentifyParams>
4 {
7 },
8 OnConflict = "merge" // "merge" | "skip" | "replace"
9 });
10
11 Console.WriteLine(result.Total); // 2
12 Console.WriteLine(result.Created); // 1
13 Console.WriteLine(result.Updated); // 1
Send a one-off email using an NLT template, without a flow.
csharp
1 var result = await client.SendAsync(new SendParams
2 {
3 TemplateSlug = "otp-verification",
4 To = new Dictionary<string, object>
5 {
6 ["user_id"] = "user_123",
8 ["first_name"] = "Jane"
9 },
10 Attributes = new Dictionary<string, object>
11 {
12 ["otp_code"] = "847293",
13 ["expiry_minutes"] = 10
14 },
15 IdempotencyKey = $"otp_user_123_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}"
16 });
17
18 Console.WriteLine(result.Status); // "sent" or "suppressed"
19 Console.WriteLine(result.EmailLogId); // "el_8f14e45f-..."
Requires a data-scoped API key (or higher). The template must exist in your workspace.
The client.Contacts sub-client provides full CRUD operations. Requires a management or full scoped API key.
csharp
1 var result = await client.Contacts.ListAsync(new ContactListParams
2 {
3 Search = "jane",
4 Page = 1,
5 PerPage = 25,
6 SortBy = "created_at",
7 SortOrder = "desc"
8 });
9
10 Console.WriteLine(result.Meta.Total); // 142
11 Console.WriteLine(result.Meta.TotalPages); // 6
12
13 foreach (var c in result.Data)
14 {
15 Console.WriteLine($"{c.Email} {c.FirstName}");
16 }
csharp
var contact = await client.Contacts.GetAsync("contact_uuid");
csharp
1 var updated = await client.Contacts.UpdateAsync("user_123", new ContactUpdateParams
2 {
4 AddTags = new List<string> { "vip" },
5 RemoveTags = new List<string> { "trial" }
6 });
csharp
await client.Contacts.DeleteAsync("user_123");
The client.Templates sub-client manages email templates. Requires a management or full scoped API key.
csharp
var templates = await client.Templates.ListAsync();
csharp
var template = await client.Templates.GetAsync("welcome-email");
csharp
1 var created = await client.Templates.CreateAsync(new TemplateCreateParams
2 {
3 Name = "Welcome Email",
4 Slug = "welcome-email",
5 Subject = "Welcome, [first name of contact]!",
6 BodyHtml = "<h1>Welcome!</h1><p>Thanks for joining.</p>",
7 SenderName = "PYRX Team",
9 });
csharp
1 var updated = await client.Templates.UpdateAsync("welcome-email", new TemplateUpdateParams
2 {
3 Subject = "Welcome aboard, [first name of contact]!"
4 });
csharp
1 var preview = await client.Templates.PreviewAsync("welcome-email", new TemplatePreviewParams
2 {
3 Contact = new Dictionary<string, object> { ["first_name"] = "Jane", ["email"] = "[email protected] " }, 4 TriggerEvent = new Dictionary<string, object> { ["order_id"] = "ord_123" }
5 });
6
7 Console.WriteLine(preview.Subject); // Rendered subject
8 Console.WriteLine(preview.Html); // Rendered HTML
9 Console.WriteLine(preview.Suppressed); // false
10 Console.WriteLine(preview.SuppressedReason); // null
csharp
await client.Templates.DeleteAsync("old-template");
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
1 using PyrxSynapse;
2 using Microsoft.AspNetCore.Mvc;
3
4 [ApiController]
5 [Route("webhooks/synapse")]
6 public class WebhookController : ControllerBase
7 {
8 [HttpPost]
9 public IActionResult HandleWebhook()
10 {
11 using var reader = new StreamReader(Request.Body);
12 var payload = reader.ReadToEndAsync().Result;
13
14 var headers = new Dictionary<string, string>
15 {
16 ["svix-id"] = Request.Headers["svix-id"].ToString(),
17 ["svix-timestamp"] = Request.Headers["svix-timestamp"].ToString(),
18 ["svix-signature"] = Request.Headers["svix-signature"].ToString()
19 };
20
21 var secret = Environment.GetEnvironmentVariable("SYNAPSE_WEBHOOK_SECRET");
22
23 try
24 {
25 var webhookEvent = Webhooks.Verify(payload, headers, secret);
26 Console.WriteLine(webhookEvent["type"]); // e.g. "email.delivered"
27 return Ok();
28 }
29 catch (InvalidOperationException e)
30 {
31 // Invalid signature, expired timestamp, or missing headers
32 Console.WriteLine($"Webhook rejected: {e.Message}");
33 return Unauthorized();
34 }
35 }
36 }
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).
The SDK provides typed exception classes for every failure mode. All errors extend SynapseException.
csharp
1 using PyrxSynapse.Errors;
2
3 try
4 {
5 await client.TrackAsync(new TrackParams
6 {
7 ExternalId = "u1",
8 EventName = "test"
9 });
10 }
11 catch (SynapsePlanLimitException e)
12 {
13 Console.WriteLine($"Plan limit: {e.LimitType} ({e.Current}/{e.Maximum})");
14 Console.WriteLine($"Current plan: {e.Plan}");
15 }
16 catch (SynapseRateLimitException e)
17 {
18 Console.WriteLine($"Rate limited. Retry after {e.RetryAfter}s");
19 }
20 catch (SynapseValidationException e)
21 {
22 foreach (var err in e.Errors)
23 {
24 Console.WriteLine($"{err.Field}: {err.Message}");
25 }
26 }
27 catch (SynapseAuthException e)
28 {
29 Console.WriteLine($"Authentication failed: {e.Message}");
30 }
31 catch (SynapseException e)
32 {
33 Console.WriteLine($"API error {e.Status}: {e.Message}");
34 }
Error Class HTTP Status Properties When SynapseExceptionAny Status, Message, Code, RequestIdBase type for all API errors SynapseAuthException401, 403 MessageInvalid or expired API key, scope mismatch SynapseValidationException422 Errors with Field + MessageRequest body validation failed SynapseRateLimitException429 RetryAfter (seconds)Rate limit exceeded (auto-retried) SynapsePlanLimitException403 LimitType, Current, Maximum, PlanPlan limit reached
Every method is available in both async and sync variants. Prefer async in production code.
csharp
1 // Async (recommended)
2 var result = await client.TrackAsync(trackParams);
3
4 // Sync (blocks the calling thread)
5 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.
For production deployments, load credentials from environment variables.
csharp
1 using PyrxSynapse;
2
3 var client = new SynapseClient(new SynapseConfig
4 {
5 ApiKey = Environment.GetEnvironmentVariable("SYNAPSE_API_KEY")!,
6 WorkspaceId = Environment.GetEnvironmentVariable("SYNAPSE_WORKSPACE_ID")!
7 });
bash
1 export SYNAPSE_API_KEY=psk_live_a1b2c3d4e5f67890abcdef1234567890
2 export SYNAPSE_WORKSPACE_ID=your_workspace_id
Method Description Required Scope client.TrackAsync(params)Track a single event dataclient.TrackBatchAsync(params)Track up to 50 events dataclient.IdentifyAsync(params)Upsert a single contact dataclient.IdentifyBatchAsync(params)Upsert up to 1,000 contacts dataclient.SendAsync(params)Send a transactional email dataclient.Contacts.ListAsync(params)List contacts with pagination managementclient.Contacts.GetAsync(id)Get a single contact managementclient.Contacts.UpdateAsync(id, params)Update a contact managementclient.Contacts.DeleteAsync(id)Delete a contact managementclient.Templates.ListAsync()List all templates managementclient.Templates.GetAsync(slug)Get a template by slug managementclient.Templates.CreateAsync(params)Create a template managementclient.Templates.UpdateAsync(slug, params)Update a template managementclient.Templates.PreviewAsync(slug, params)Preview rendered template managementclient.Templates.DeleteAsync(slug)Delete a template managementWebhooks.Verify(payload, headers, secret)Verify webhook signature --
csharp
1 // Program.cs
2 using PyrxSynapse;
3 using PyrxSynapse.Models;
4
5 var builder = WebApplication.CreateBuilder(args);
6
7 builder.Services.AddSingleton(new SynapseClient(new SynapseConfig
8 {
9 ApiKey = Environment.GetEnvironmentVariable("SYNAPSE_API_KEY")!,
10 WorkspaceId = Environment.GetEnvironmentVariable("SYNAPSE_WORKSPACE_ID")!
11 }));
12
13 var app = builder.Build();
14
15 app.MapPost("/signup", async (SignupRequest req, SynapseClient synapse) =>
16 {
17 // Identify the new user
18 await synapse.IdentifyAsync(new IdentifyParams
19 {
20 ExternalId = req.UserId,
21 Email = req.Email,
22 Properties = new Dictionary<string, object> { ["plan"] = req.Plan },
23 Tags = new List<string> { "new-signup" }
24 });
25
26 // Track the signup event (triggers flows)
27 await synapse.TrackAsync(new TrackParams
28 {
29 ExternalId = req.UserId,
30 EventName = "user_signed_up",
31 Attributes = new Dictionary<string, object> { ["plan"] = req.Plan, ["source"] = "web" }
32 });
33
34 return Results.Ok(new { success = true });
35 });
36
37 app.Run();
38
39 record SignupRequest(string UserId, string Email, string Plan);
csharp
1 using Microsoft.AspNetCore.Mvc;
2 using PyrxSynapse;
3
4 [ApiController]
5 [Route("api/[controller]")]
6 public class EventsController : ControllerBase
7 {
8 private readonly SynapseClient _synapse;
9
10 public EventsController(SynapseClient synapse)
11 {
12 _synapse = synapse;
13 }
14
15 [HttpPost("track")]
16 public async Task<IActionResult> Track([FromBody] TrackRequest req)
17 {
18 var result = await _synapse.TrackAsync(new TrackParams
19 {
20 ExternalId = req.UserId,
21 EventName = req.EventName,
22 Attributes = req.Attributes
23 });
24
25 return Ok(new { eventId = result.EventId });
26 }
27 }
28
29 public record TrackRequest(string UserId, string EventName, Dictionary<string, object> Attributes);