C# SDK
Installation
dotnet add package Rheo.SdkNuGet\Install-Package Rheo.Sdk<PackageReference Include="Rheo.Sdk" Version="0.7.*" />Requires .NET 7 or later.
Setup
using Rheo.Sdk;
var rheo = new RheoClient(new RheoClientOptions{ ApiKey = Environment.GetEnvironmentVariable("RHEO_API_KEY")!, WebhookSecret = Environment.GetEnvironmentVariable("RHEO_WEBHOOK_SECRET"), // optional});| Property | Type | Default | Description |
|---|---|---|---|
ApiKey | string | required | API key from the Rheo partner dashboard |
WebhookSecret | string? | null | whsec_... secret for webhook signature verification |
BaseUrl | Uri | https://market.rheo.se | Override for staging or self-hosted |
Timeout | TimeSpan | 30 s | Per-request timeout |
MaxRetries | int | 3 | Max retries on 429 / 5xx |
PartnerAccount | string? | null | Reseller member ID — scopes all item calls to that account |
RheoClient implements IDisposable. In long-lived applications, register it as a singleton in your DI container.
// ASP.NET Core DIbuilder.Services.AddSingleton(_ => new RheoClient(new RheoClientOptions{ ApiKey = builder.Configuration["Rheo:ApiKey"]!, WebhookSecret = builder.Configuration["Rheo:WebhookSecret"],}));Items
Push a single item
var result = await rheo.Items.UpsertAsync("ERP_PART_12345", new UpsertItemRequest{ Title = "Volvo XC90 bromsok fram vänster — 2018", Description = "Testad och fungerande. Skick B.", Price = 950, ShippingCost = 149, WeightKg = 3.2, ImageUrls = [ "https://cdn.example.com/parts/12345/1.jpg", "https://cdn.example.com/parts/12345/2.jpg", ], Domain = new AutoPartsDomain { Vehicle = new DonorVehicle { Manufacturer = "Volvo", Model = "XC90", Year = 2018, VehicleType = "Bil", }, Part = new PartInfo { Name = "Bromsok fram vänster", OemNumber = "31400452", CatalogCodes = new Dictionary<string, string> { ["recopart"] = "7118" }, }, ConditionGrade = "B", }, AutoPublishTradera = true, UseAiEnhancement = true,});// result.Success == true// result.ItemId — Rheo's internal UUIDUpsertAsync creates or updates by external ID. Pass a CancellationToken as the final argument for request cancellation.
Get item status
var item = await rheo.Items.GetAsync("ERP_PART_12345");// item.RheoStatus — RheoItemStatus enum: Draft, Active, Sold, NotListed, Archived// item.TraderaAdUrl — direct link to the live Tradera auction// item.Price — current price in SEKUpdate price
await rheo.Items.UpdatePriceAsync("ERP_PART_12345", new UpdatePriceRequest { Price = 850 });Update status
await rheo.Items.UpdateStatusAsync("ERP_PART_12345", new UpdateStatusRequest { Status = "sold" });Delete
await rheo.Items.DeleteAsync("ERP_PART_12345");Batch operations
Push up to 500 items in a single call.
var parts = await dms.GetPartsForVehicleAsync("YV1CZ714581234567");
var result = await rheo.Items.BatchUpsertAsync(new BatchUpsertRequest{ Items = parts.Select(p => new BatchUpsertItem { ExternalId = $"VIN-YV1CZ714581234567-{p.PartNumber}", ParentExternalId = "VIN-YV1CZ714581234567", Title = p.Name, Description = p.Notes, Price = p.AskingPrice, ShippingCost = 149, ImageUrls = p.Photos.Select(ph => ph.Url).ToList(), Domain = new AutoPartsDomain { Vehicle = new DonorVehicle { Manufacturer = "Volvo", VehicleType = "Bil" }, Part = new PartInfo { Name = p.Name, OemNumber = p.OemNumber }, ConditionGrade = p.Grade, }, Metadata = new Dictionary<string, object?> { ["locationBin"] = p.BinLocation, ["dismantledBy"] = p.TechnicianId, }, AutoPublishTradera = p.Grade != "C", }).ToList(),});
Console.WriteLine($"Accepted: {result.Accepted}, rejected: {result.Rejected}");foreach (var err in result.Errors) Console.Error.WriteLine($"{err.ExternalId}: {err.Reason}");Querying items
// Active parts for a specific vehiclevar page = await rheo.Items.ListAsync(new ListItemsParams{ ParentExternalId = "VIN-YV1CZ714581234567", Status = RheoItemStatus.Active, Limit = 100,});
// Incremental sync — changes in last 24 hoursvar updated = await rheo.Items.ListAsync(new ListItemsParams{ UpdatedSince = DateTimeOffset.UtcNow.AddDays(-1),});
// Cursor paginationstring? cursor = null;do{ var p = await rheo.Items.ListAsync(new ListItemsParams { Limit = 100, Cursor = cursor }); // process p.Items ... cursor = p.NextCursor;} while (cursor is not null);Container summary
var summary = await rheo.Items.SummaryAsync("VIN-YV1CZ714581234567");Console.WriteLine($"Parts live: {summary.ActiveCount} / {summary.ChildCount}");Console.WriteLine($"Listed value: {summary.ListedValue:N0} {summary.Currency}");Console.WriteLine($"Sold value: {summary.SoldValue:N0} {summary.Currency}");List children
var result = await rheo.Items.ChildrenAsync("VIN-YV1CZ714581234567");foreach (var c in result.Children) Console.WriteLine($"{c.ExternalId} — {c.Status} — {c.Price} SEK");Item history
var history = await rheo.Items.HistoryAsync("ERP_PART_12345"); // newest firstforeach (var e in history.Events) Console.WriteLine($"{e.CreatedAt:O} — {e.EventType}");Vehicle hierarchy
// Register the donor vehicle as a containerawait rheo.Items.UpsertAsync("VIN-YV1CZ714581234567", new UpsertItemRequest{ Type = RheoItemType.Container, Title = "Volvo XC90 II 2018 — VIN YV1CZ714581234567", Metadata = new Dictionary<string, object?> { ["vin"] = "YV1CZ714581234567", ["make"] = "Volvo", ["model"] = "XC90 II", ["year"] = 2018, ["receivedAt"] = DateTimeOffset.UtcNow.ToString("O"), },});
// Push all parts in one batchawait rheo.Items.BatchUpsertAsync(new BatchUpsertRequest{ Items = parts.Select(p => new BatchUpsertItem { ExternalId = $"VIN-YV1CZ714581234567-{p.PartNumber}", ParentExternalId = "VIN-YV1CZ714581234567", Type = RheoItemType.Item, // ... }).ToList(),});Reseller routing
If you manage several Rheo accounts under one reseller API key (e.g. a chain of
dismantling yards), use ForAccount to route calls to a specific member. The value is
the reseller’s external_id for that member — the reference you set in the business
Managed accounts UI. The returned scope sends x-partner-account per request and
overrides any client-level PartnerAccount:
// One client + key, many yardsawait rheo.ForAccount("10").Items.UpsertAsync("10-PART-001", data);await rheo.ForAccount("10").Items.UpdatePriceAsync("10-PART-001", new UpdatePriceRequest { Price = 800 });var yard = await rheo.ForAccount("10").Items.ListAsync(new ListItemsParams { Status = RheoItemStatus.Active });ForAccount reuses the client’s underlying HttpClient — it is cheap to call per
request and you should not dispose it. Set PartnerAccount on RheoClientOptions
instead to scope every call to a single account.
See the Reseller model for how membership and billing work.
Webhooks
ASP.NET Core minimal API
app.MapPost("/webhooks/rheo", async (HttpRequest request, RheoClient rheo) =>{ using var reader = new StreamReader(request.Body); var rawBody = await reader.ReadToEndAsync(); var signature = request.Headers["X-Rheo-Signature"].ToString();
RheoEvent evt; try { evt = rheo.Webhooks.Verify(rawBody, signature); } catch (RheoWebhookSignatureException) { return Results.Unauthorized(); }
switch (evt) { case ItemSoldEvent sold: // sold.Data.Account?.MemberExternalId tells you which yard (reseller endpoints) await dms.MarkPartSoldAsync(sold.Data.ExternalId, sold.Data.SalePrice); break; case ListingCreatedEvent listed: await dms.RecordTraderaUrlAsync(listed.Data.ExternalId, listed.Data.TraderaAdUrl); break; case ListingFailedEvent failed: await alerts.NotifyAsync($"Listing failed for {failed.Data.ExternalId}: {failed.Data.Error}"); break; }
return Results.Ok();});Classic controller
[ApiController][Route("webhooks")]public sealed class WebhooksController : ControllerBase{ private readonly RheoClient _rheo;
public WebhooksController(RheoClient rheo) => _rheo = rheo;
[HttpPost("rheo")] public async Task<IActionResult> Receive() { using var reader = new StreamReader(Request.Body); var rawBody = await reader.ReadToEndAsync(); var signature = Request.Headers["X-Rheo-Signature"].ToString();
RheoEvent evt; try { evt = _rheo.Webhooks.Verify(rawBody, signature); } catch (RheoWebhookSignatureException) { return Unauthorized(); }
// dispatch evt... return Ok(); }}Read the raw request body before any JSON middleware parses it. Pass the original bytes to Verify() — re-serializing a deserialized object will fail the HMAC check.
Per-endpoint secrets
Each webhook endpoint has its own signing secret. Verify with the secret of the
endpoint that received the event. Pass one or more secrets to Verify() (they override
the client-level WebhookSecret) — any match succeeds, which is handy during rotation:
var evt = rheo.Webhooks.Verify(rawBody, signature, endpointSecret);var rotating = rheo.Webhooks.Verify(rawBody, signature, oldSecret, newSecret);See Webhooks for managing multiple endpoints, event-type
filters, and the account field added to every payload for reseller routing.
Error handling
using Rheo.Sdk.Exceptions;
try{ await rheo.Items.UpsertAsync("PART_001", new UpsertItemRequest { Title = "Test part", Price = 100, ShippingCost = 50, });}catch (RheoRateLimitException ex){ var delay = ex.RetryAfter ?? TimeSpan.FromSeconds(5); await Task.Delay(delay); // retry...}catch (RheoApiException ex){ logger.LogError("Rheo API error {Status}: {Message} (requestId: {RequestId})", ex.Status, ex.Message, ex.RequestId);}The SDK retries 429 and 5xx responses automatically (up to MaxRetries). RheoRateLimitException is only thrown if retries are exhausted.
Cancellation
Every method accepts an optional CancellationToken as its last parameter.
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var item = await rheo.Items.GetAsync("ERP_PART_12345", cts.Token);Types reference
// Item status enumenum RheoItemStatus { Draft, Active, Sold, NotListed, Archived }
// Item typeenum RheoItemType { Item, Container }
// Domain objects (polymorphic on the "domain" discriminator)abstract class DomainObject { string Domain } // slug accessor, not serializedclass DonorVehicle { Manufacturer, Model, Year, VehicleType }class PartInfo { Name, OemNumber, CatalogCodes (IReadOnlyDictionary<string,string>) }
// Rich domains (typed attributes)sealed class AutoPartsDomain : DomainObject { Vehicle (DonorVehicle), Part (PartInfo), ConditionGrade, ConditionNote }sealed class VehiclesDomain : DomainObject { RegistrationPlate, Make, Model, Year, MileageKm, FuelType, Color }sealed class ElectronicsDomain : DomainObject { SerialNumber, MemoryGb, BatteryHealthPercentage }sealed class ComputersDomain : DomainObject { SerialNumber, MemoryGb, BatteryHealthPercentage }sealed class FashionDomain : DomainObject { Size, Material, Gender }sealed class BooksDomain : DomainObject { Isbn, Author, Publisher, Language }sealed class WatchesDomain : DomainObject { Brand, ModelReference, MovementType, CaseSizeMm }
// Unit domains (discriminant only): HomeGardenDomain, ArtDomain, SportsDomain,// ToysDomain, ToolsDomain, JewelryDomain, MusicDomain, CollectiblesDomain,// BeautyDomain, IndustrialDomain, OtherDomain
// Event types (use pattern matching). Every event has { EventId, Timestamp, EventType, Data }.abstract class RheoEvent { EventId, Timestamp, EventType, Data (EventData) } ├── ItemCreatedEvent // Data.Type: "item" | "container" ├── ItemImagesReadyEvent // Data.ImagesProcessed ├── ListingCreatedEvent // Data.TraderaAdId, Data.TraderaAdUrl ├── ListingEndedEvent // Data.TraderaAdId ├── ListingFailedEvent // Data.Error └── ItemSoldEvent // Data.TraderaAdId
class EventData { ExternalId, RheoItemId, Platform, SalePrice, Currency, Account (EventAccount?), // memberExternalId on reseller (members) endpoints Type?, ImagesProcessed?, TraderaAdId?, TraderaAdUrl?, Error? // populated per event}class EventAccount { RheoUserId, MemberExternalId? }Deserialize using the SDK’s Webhooks.Verify() / Webhooks.ParseEvent() — they use a
converter that reads the eventType discriminator at any position in the payload.