Bento .NET Guide
This guide walks through installing the Bento .NET SDK, wiring it into your dependency injection container, and shipping production-ready automations.
Implement Bento's .NET SDK end-to-end: install the client, track events, manage subscribers, and deliver transactional emails with confidence.
Getting Started
Beginner Guide
Intermediate Guide
Advanced Guide
Reference
Getting Started
Step 1
Install the Bento .NET SDK
Install the SDK with your framework’s package manager and keep it alongside your other HTTP clients so configuration, logging, and credentials stay centralized.
Using NuGet Package Manager or Manual Installation
xml<ItemGroup>
<Reference Include="Bento">
<HintPath>path/to/extracted/zip/Bento.dll</HintPath>
</Reference>
</ItemGroup>Step 2
Configure the client
Store your Site UUID, publishable key, and secret key in environment variables or a secrets manager. Register the Bento services during your app’s bootstrap so every controller, job, or command reuses the same client.
basic_setup
csharpusing Bento;
using Bento.Extensions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
// Configure services in Program.cs or Startup.cs
var configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.Build();
var services = new ServiceCollection();
services.AddBentoClient(configuration);
// In appsettings.json:
{
"Bento": {
"PublishableKey": "your_publishable_key",
"SecretKey": "your_secret_key",
"SiteUuid": "your_site_uuid"
}
}Beginner Guide
Tracking your first event
Events are the fastest way to add subscribers, kick off automations, and capture context in a single API call. Track onboarding milestones, purchases, or page views, then personalize with Liquid.
- Pair every event with subscriber metadata so flows can branch without extra imports.
- Include transactional context (cart items, amounts, IDs) for downstream personalization.
- Prefer events over bespoke subscriber mutations—automations can add tags or update fields later.
| Detail | Liquid tag |
|---|---|
| Product details | {{ event.details.product.size }} |
| Purchase amount | {{ event.details.value.amount }} |
| Transaction ID | {{ event.details.unique.key }} |
| Payment method | {{ event.details.value.payment_method }} |
Track a simple page view
csharpvar eventRequest = new EventRequest(
Type: "$pageView",
Email: "user@example.com",
Details: new Dictionary<string, object>
{
{ "url", "/home" },
{ "title", "Home Page" }
}
);
var response = await _eventService.TrackEventAsync<dynamic>(eventRequest);
if (!response.Success)
{
// Handle error
}Track a form submission
csharpvar eventRequest = new EventRequest(
Type: "$formSubmitted",
Email: "user@example.com",
Details: new Dictionary<string, object>
{
{ "formName", "Newsletter Signup" },
{ "source", "Homepage" }
}
);
var response = await _eventService.TrackEventAsync<dynamic>(eventRequest);
if (!response.Success)
{
// Handle error
}Managing subscribers
You can still create, tag, or unsubscribe subscribers directly when you need to correct data or build admin tooling. Keep these calls reserved for operational work—events remain the preferred entry point.
Create a new subscriber
csharpvar subscriberRequest = new SubscriberRequest(
Email: "new@example.com"
);
var response = await _subscriberService.CreateSubscriberAsync<dynamic>(subscriberRequest);
if (!response.Success)
{
// Handle error
}Tag a subscriber
csharpvar subscribers = new List<SubscriberRequest>
{
new(
Email: "user@example.com",
Tags: new[] { "Newsletter" }
)
};
var response = await _subscriberService.ImportSubscribersAsync<dynamic>(subscribers);
if (!response.Success)
{
// Handle error
}Unsubscribe A subscriber
csharpvar command = new CommandRequest(
Command: "unsubscribe",
Email: "user@example.com",
Query: "" // No query needed for unsubscribe
);
var response = await _commandService.ExecuteCommandAsync<dynamic>(command);
if (!response.Success)
{
// Handle error
}Common use cases
- Track onboarding, purchase, and lifecycle events so automations fire with one API call.
- Tag subscribers in response to events rather than separate API loops.
- Store contextual data (cart contents, plan tier) for segmentation and personalization.
Tracking a user login "Event"
csharpvar eventRequest = new EventRequest(
Type: "$login",
Email: "user@example.com",
Details: new Dictionary<string, object>
{
{ "method", "password" },
{ "device", "mobile" }
}
);
var response = await _eventService.TrackEventAsync<dynamic>(eventRequest);
if (!response.Success)
{
// Handle error
}Updating user information
csharpvar eventRequest = new EventRequest(
Type: "$activation",
Email: "user@example.com",
Details: new Dictionary<string, object>
{
{ "account", "active" },
{ "device", "mobile" }
}
);
var response = await _eventService.TrackEventAsync<dynamic>(eventRequest);
if (!response.Success)
{
// Handle error
}Adding multiple tags to a subscriber
csharpvar subscribers = new List<SubscriberRequest>
{
new(
Email: "user@example.com",
Tags: new[] { "Premium", "Annual Plan", "Early Adopter" }
)
};
var response = await _subscriberService.ImportSubscribersAsync<dynamic>(subscribers);
if (!response.Success)
{
// Handle error
}Intermediate Guide
Custom fields and tags
Combine fields for rich profile data with namespaced tags for segmentation. Tags keep audiences organized (`plan:pro`, `status:vip`) while fields store free-form values like locale or favorite product.
Namespaced tag ideas
Apply via automations whenever possible so marketing and data teams stay in sync.
Create a new custom field definition
csharpvar fieldRequest = new FieldRequest("membershipLevel");
var response = await _fieldService.CreateFieldAsync<dynamic>(fieldRequest);
if (!response.Success)
{
// Handle error
}Get all existing fields
csharpvar response = await _fieldService.GetFieldsAsync<dynamic>();
if (!response.Success)
{
// Handle error
}
// Now you can access the fields data
var fields = response.Data;Create a new tag
csharpvar tagRequest = new TagRequest("Power User");
var response = await _tagService.CreateTagAsync<dynamic>(tagRequest);
if (!response.Success)
{
// Handle error
}Get all existing tags
csharpvar response = await _tagService.GetTagsAsync<dynamic>();
if (!response.Success)
{
// Handle error
}
// Now you can access the tags data
var tags = response.Data;Tracking purchase events
Always include a `unique` key (order ID, cart token, etc.) so Bento dedupes purchases. Capture cart contents and totals to power LTV reporting and Liquid personalization.
Track a purchase event to monitor customer lifetime value
csharpvar eventRequest = new EventRequest(
Type: "$purchase",
Email: "customer@example.com",
Details: new Dictionary<string, object>
{
{
"unique", new Dictionary<string, object>
{
{ "key", "order-123" } // Unique order identifier
}
},
{
"value", new Dictionary<string, object>
{
{ "amount", 9999 }, // Amount in cents
{ "currency", "USD" }
}
},
{
"cart", new Dictionary<string, object>
{
{
"items", new[]
{
new Dictionary<string, object>
{
{ "product_id", "prod-456" },
{ "product_name", "Premium Widget" },
{ "product_price", 9999 },
{ "quantity", 1 },
{ "product_sku", "SKU-456" }
}
}
}
}
}
}
);
var response = await _eventService.TrackEventAsync<dynamic>(eventRequest);
if (!response.Success)
{
// Handle error
}Namespaced tags
Reserve tags for segmentation keys (e.g., `product:analytics`). Use fields when you need arbitrary values or timestamps. This keeps your marketing taxonomy predictable.
Advanced Guide
Batch operations
Batch endpoints cover more than 80% of API work. Keep payloads lean, batch 200–300 records, and retry only failed chunks.
Import multiple subscribers at once
csharpvar subscribers = new List<SubscriberRequest>
{
new(
Email: "user1@example.com",
FirstName: "Alice",
Fields: new Dictionary<string, object>
{
{ "company", "Acme Inc" }
}
),
new(
Email: "user2@example.com",
FirstName: "Bob",
Fields: new Dictionary<string, object>
{
{ "company", "Beta Corp" }
}
)
// Can add up to 1,000 subscribers in a single batch
};
var response = await _subscriberService.ImportSubscribersAsync<dynamic>(subscribers);
if (!response.Success)
{
// Handle error
}Import multiple events at once
csharpvar events = new List<EventRequest>
{
new(
Type: "$login",
Email: "user@example.com",
Fields: new Dictionary<string, object>
{
{ "date", "2023-01-01" }
}
),
new(
Type: "$purchase",
Email: "user@example.com",
Details: new Dictionary<string, object>
{
{
"unique", new Dictionary<string, object>
{
{ "key", "order-123" }
}
},
{
"value", new Dictionary<string, object>
{
{ "currency", "USD" },
{ "amount", 9999 }
}
}
}
)
// Can add up to 1,000 events in a single batch
};
var response = await _eventService.TrackEventsAsync<dynamic>(events);
if (!response.Success)
{
// Handle error
}Transactional emails
Great candidates
- Onboarding confirmation or welcome sequences
- Password reset and login verification links
- Payment, receipt, or fulfillment notices
- Account or compliance notifications
Avoid using transactional for
- CC / BCC workflows or multi-recipient fan-out
- Attachments (link to files instead)
- Marketing, promotional, or newsletter content
- Bulk announcements where unsubscribes must be honored
Send a transactional email
csharpvar emailRequest = new EmailRequest(
To: "recipient@example.com",
From: "sender@example.com",
Subject: "Reset Password",
HtmlBody: "<p>Here is a link to reset your password ... {{ link }}</p>",
Transactional: true
);
var response = await _emailService.SendEmailAsync<dynamic>(emailRequest);
if (!response.Success)
{
// Handle error
}
Console.WriteLine("Successfully queued email for delivery");Subscriber updates
Events remain the preferred way to create and update subscribers—they trigger flows and can mutate tags or fields downstream. Use direct endpoints when workflows demand determinism.
Events-first creation
Create a subscriber when they sign up
csharpvar eventRequest = new EventRequest(
Type: "$subscribe",
Email: "new-user@example.com",
Fields: new Dictionary<string, object>
{
{ "firstName", "Jane" },
{ "lastName", "Doe" },
{ "signupSource", "website" }
}
);
var response = await _eventService.TrackEventAsync<dynamic>(eventRequest);
if (!response.Success)
{
// Handle error
}Create a subscriber when they make a purchase
csharpvar eventRequest = new EventRequest(
Type: "$purchase",
Email: "customer@example.com",
Details: new Dictionary<string, object>
{
{
"unique", new Dictionary<string, object>
{
{ "key", "order-123" }
}
},
{
"value", new Dictionary<string, object>
{
{ "amount", 9999 },
{ "currency", "USD" }
}
}
},
Fields: new Dictionary<string, object>
{
{ "firstName", "John" },
{ "lastName", "Smith" },
{ "customerType", "new" }
}
);
var response = await _eventService.TrackEventAsync<dynamic>(eventRequest);
if (!response.Success)
{
// Handle error
}Direct creation options
Create a single subscriber (email only)
csharpvar subscriberRequest = new SubscriberRequest(
Email: "user@example.com"
);
var response = await _subscriberService.CreateSubscriberAsync<dynamic>(subscriberRequest);
if (!response.Success)
{
// Handle error
}Import multiple subscribers
csharpvar subscribers = new List<SubscriberRequest>
{
new(
Email: "user1@example.com",
Fields: new Dictionary<string, object>
{
{ "membershipTier", "gold" },
{ "accountStatus", "active" },
{ "lastRenewalDate", DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss.fffZ") }
}
),
new(
Email: "user2@example.com",
Fields: new Dictionary<string, object>
{
{ "membershipTier", "silver" },
{ "accountStatus", "pending" },
{ "trialEndsAt", DateTime.Now.AddDays(30).ToString("yyyy-MM-ddTHH:mm:ss.fffZ") }
}
)
};
var response = await _subscriberService.ImportSubscribersAsync<dynamic>(subscribers);
if (!response.Success)
{
// Handle error
}Event-driven profile updates
Update subscriber when they update their profile
csharpvar eventRequest = new EventRequest(
Type: "$subscription_change",
Email: "user@example.com",
Fields: new Dictionary<string, object>
{
{ "subscriptionTier", "premium" }
}
);
var response = await _eventService.TrackEventAsync<dynamic>(eventRequest);
if (!response.Success)
{
// Handle error
}Single attribute tweaks
Add or update a single field
csharpvar subscribers = new List<SubscriberRequest>
{
new(
Email: "user2@example.com",
FirstName: "Jesse",
LastName: "Bento",
Tags: new[] { "membership:premium" },
RemoveTags: new[] { "membership:silver" },
Fields: new Dictionary<string, object>
{
{ "membershipTier", "premium" },
{ "accountStatus", "pending" },
{ "trialEndsAt", DateTime.Now.AddDays(30).ToString("yyyy-MM-ddTHH:mm:ss.fffZ") }
}
)
};
var response = await _subscriberService.ImportSubscribersAsync<dynamic>(subscribers);
if (!response.Success)
{
// Handle error
}Add a tag
csharpvar subscribers = new List<SubscriberRequest>
{
new(
Email: "user2@example.com",
FirstName: "Jesse",
LastName: "Bento",
Tags: new[] { "example:tag" }
)
};
var response = await _subscriberService.ImportSubscribersAsync<dynamic>(subscribers);
if (!response.Success)
{
// Handle error
}Remove a field
csharpvar command = new CommandRequest(
Command: "remove_field",
Email: "user@example.com",
Query: "temporaryStatus"
);
var response = await _commandService.ExecuteCommandAsync<dynamic>(command);
if (!response.Success)
{
// Handle error
}Batch updates
Update multiple subscribers
csharpvar subscribers = new List<SubscriberRequest>
{
new(
Email: "user1@example.com",
Fields: new Dictionary<string, object>
{
{ "membershipTier", "gold" },
{ "accountStatus", "active" },
{ "lastRenewalDate", DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss.fffZ") }
}
),
new(
Email: "user2@example.com",
Fields: new Dictionary<string, object>
{
{ "membershipTier", "silver" },
{ "accountStatus", "pending" },
{ "trialEndsAt", DateTime.Now.AddDays(30).ToString("yyyy-MM-ddTHH:mm:ss.fffZ") }
}
)
};
var response = await _subscriberService.ImportSubscribersAsync<dynamic>(subscribers);
if (!response.Success)
{
// Handle error
}Specialized operations
Change a subscribers email address
csharpvar command = new CommandRequest(
Command: "change_email",
Email: "old@example.com",
Query: "new@example.com"
);
var response = await _commandService.ExecuteCommandAsync<dynamic>(command);
if (!response.Success)
{
// Handle error
}Update all fields at once
csharpvar subscribers = new List<SubscriberRequest>
{
new(
Email: "user@example.com",
FirstName: "Updated",
LastName: "Name",
Fields: new Dictionary<string, object>
{
{
"address", new Dictionary<string, object>
{
{ "street", "123 Main St" },
{ "city", "New York" },
{ "state", "NY" },
{ "zip", "10001" }
}
},
{
"preferences", new Dictionary<string, object>
{
{ "theme", "dark" },
{ "notifications", true }
}
}
}
)
};
var response = await _subscriberService.ImportSubscribersAsync<dynamic>(subscribers);
if (!response.Success)
{
// Handle error
}Unsubscribe a user
csharpvar command = new CommandRequest(
Command: "unsubscribe",
Email: "user@example.com",
Query: "" // No query needed for unsubscribe
);
var response = await _commandService.ExecuteCommandAsync<dynamic>(command);
if (!response.Success)
{
// Handle error
}Utility features
Validate addresses, guess gender, geolocate IPs, or check blacklists without integrating third-party APIs. These helpers are rate-limited—cache responses when you can.
Validate Email
csharpvar validationRequest = new EmailValidationRequest(
EmailAddress: "user@example.com",
FullName: "John Doe", // Optional
UserAgent: "Mozilla/5.0...", // Optional
IpAddress: "192.168.1.1" // Optional
);
var response = await _validationService.ValidateEmailAsync<dynamic>(validationRequest);
if (!response.Success)
{
// Handle error
}
Console.WriteLine($"Email validation result: {response.Data}");Guess Prediction (for personalization)
csharpvar genderRequest = new GenderRequest("Alex");
var response = await _genderService.PredictGenderAsync<dynamic>(genderRequest);
if (!response.Success)
{
// Handle error
}
Console.WriteLine($"Gender prediction: {response.Data}");Geolocate IP Address
csharpvar geolocationRequest = new GeolocationRequest("208.67.222.222");
var response = await _geolocationService.GeolocateIpAsync<dynamic>(geolocationRequest);
if (!response.Success)
{
// Handle error
}
Console.WriteLine($"Location: {response.Data}");Check Domain/IP blacklist
csharpvar blacklistRequest = new BlacklistStatusRequest(
Domain: "example.com",
IpAddress: null // Or check an IP instead: Domain: null, IpAddress: "192.168.1.1"
);
var response = await _blacklistService.GetBlacklistStatusAsync<dynamic>(blacklistRequest);
if (!response.Success)
{
// Handle error
}
Console.WriteLine($"Blacklist status: {response.Data}");API Reference
The .NET SDK mirrors Bento's REST resources—most services expose async methods for batch operations plus convenience helpers for validation.
Core operations
-
Add a subscriber
subscriberService.CreateSubscriberAsync(subscriber)
-
Bulk update subscribers
subscriberService.ImportSubscribersAsync(subscribers)
-
Track custom events
eventService.TrackEventAsync(eventRequest)
Batch endpoints
-
Import multiple subscribers
subscriberService.ImportSubscribersAsync(subscribers)
Import subscribers -
Import multiple events
eventService.TrackEventsAsync(events)
Import events -
Send transactional emails
emailService.SendBatchEmailsAsync(emails)
Emails API
Utility + experimental
-
Validate email syntax/domain
validationService.ValidateEmailAsync(request)
Validate email -
Predict gender from name
genderService.PredictGenderAsync(request)
Guess gender -
Geolocate IPs
geolocationService.GeolocateIpAsync(request)
Geolocate IP -
Check blacklist status
blacklistService.GetBlacklistStatusAsync(request)
Blacklist search -
Moderate content
moderationService.ModerateContentAsync(request)
Content moderation
Fields & tags
-
List fields
fieldService.GetFieldsAsync()
Fields -
Create field
fieldService.CreateFieldAsync(field)
Create field -
List tags
tagService.GetTagsAsync()
Tags -
Create tag
tagService.CreateTagAsync(tag)
Create tag
Broadcasts & stats
-
Fetch broadcasts
broadcastService.GetBroadcastsAsync()
Broadcasts -
Create broadcast
broadcastService.CreateBroadcastAsync(broadcast)
Create broadcast -
Create batch broadcasts
broadcastService.CreateBatchBroadcastsAsync(broadcasts)
Batch broadcasts -
Site stats
statsService.GetSiteStatsAsync()
Site stats -
Segment stats
statsService.GetSegmentStatsAsync(segmentId)
Segment stats -
Report stats
statsService.GetReportStatsAsync(reportId)
Report stats
Troubleshooting
Not Authorized
Verify publishable + secret keys and ensure the team member still has access.
Rate Limited
Implement exponential backoff for batch operations and honor Retry-After headers.
Network Errors
Confirm outbound traffic can reach app.bentonow.com and retry transient socket resets.
Payload Exceptions
Double-check Author emails and ensure payloads match the documented schema.
Debugging tips
- Enable structured logging (ILogger, Serilog, etc.) around every Bento service call so you capture request IDs and payload metadata when debugging.
- Wrap SDK calls in a Polly retry policy to throttle retries, fallback gracefully, and surface rate-limit headers in telemetry.
- Start batch imports with 200-record chunks before scaling toward 1,000 so payload or schema issues are isolated quickly.
FAQ
Can I use this SDK in a frontend environment?
No. The .NET SDK expects your publishable key, secret key, and Site UUID—credentials that must stay on the server. Use the JavaScript SDK or proxy requests through your own authenticated API when you need client-side tracking.
How do I handle rate limiting?
Treat 429 responses as transient. Use exponential backoff with jitter (Polly's `WaitAndRetryAsync` works well) and log the `Retry-After` header so ops dashboards show saturation trends.
rate_limiting
csharpusing Polly;
using Polly.Extensions.Http;
// Create a retry policy with exponential backoff
var retryPolicy = HttpPolicyExtensions
.HandleTransientHttpError()
.WaitAndRetryAsync(
retryCount: 3,
sleepDurationProvider: retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
// Use the policy when making requests
var response = await retryPolicy.ExecuteAsync(async () =>
{
return await _eventService.TrackEventAsync<dynamic>(eventRequest);
});What's the maximum batch size for importing subscribers or events?
The API accepts up to 1,000 subscribers or events per request. In practice 200–300 records keeps requests snappy and easier to retry. For very large imports, fan out work with `Task.WhenAll` and queue batches.
max_batch_size
csharp// Split subscribers into batches of 200
var batchSize = 200;
var batches = allSubscribers
.Select((subscriber, index) => new { subscriber, index })
.GroupBy(x => x.index / batchSize)
.Select(g => g.Select(x => x.subscriber).ToList())
.ToList();
// Process batches with limited concurrency
var semaphore = new SemaphoreSlim(3, 3); // Limit to 3 concurrent requests
var tasks = batches.Select(async batch =>
{
await semaphore.WaitAsync();
try
{
var response = await _subscriberService.ImportSubscribersAsync<dynamic>(batch);
if (!response.Success)
{
Console.WriteLine($"Batch import error: {response.Error}");
}
}
finally
{
semaphore.Release();
}
});
await Task.WhenAll(tasks);How do I track anonymous users?
The current SDK requires an email address for every event or subscriber mutation. Anonymous tracking support is on the roadmap—today, capture emails as early as possible (gated content, quizzes, etc.).
Which .NET versions are supported?
Target .NET 8.0 or newer so you inherit the SDK's async improvements, Span-based serializers, and low-allocation HTTP stack.
How can I contribute to the SDK?
Open pull requests or issues at github.com/bentonow/bento-dotnet-sdk. Adapters, telemetry hooks, and bug repros are all welcome—just include steps to verify your change.
How do I handle timeouts and cancellation tokens?
Inject `CancellationToken` from ASP.NET Core (or your worker host) into every SDK call. Combine it with `HttpClientFactory` policies so requests never hang longer than your queue or HTTP pipeline allows.
context_support
csharp// Create a cancellation token with timeout
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
try
{
// Use the cancellation token with SDK calls
var response = await _subscriberService.FindSubscriberAsync<dynamic>("example@test.com");
if (!response.Success)
{
// Handle errors
}
}
catch (OperationCanceledException)
{
// Handle timeout
}Can I use the SDK with serverless platforms?
Yes. Configure the client inside a static field so Azure Functions or Lambda reuse sockets between invocations. Keep payloads under size limits and break huge batches into smaller chunks to avoid cold-start penalties.
serverless_support
csharppublic class Function
{
private static readonly Lazy<IServiceProvider> _serviceProvider =
new Lazy<IServiceProvider>(BuildServiceProvider);
private static IServiceProvider BuildServiceProvider()
{
var configuration = new ConfigurationBuilder()
.AddEnvironmentVariables()
.Build();
var services = new ServiceCollection();
services.AddBentoClient(configuration);
return services.BuildServiceProvider();
}
public async Task<string> FunctionHandler(LambdaEvent lambdaEvent, ILambdaContext context)
{
var eventService = _serviceProvider.Value.GetRequiredService<IBentoEventService>();
// Use eventService for Bento operations
return "Success";
}
}Contribute or debug further
The .NET SDK is open source. Report bugs, request features, or open pull requests at https://github.com/bentonow/bento-dotnet-sdk. Keep your local tooling on .NET 8.0 or higher to match production builds.
Need the original Markdown? Open raw file