Skip to content

C# SDK

Installation

Terminal window
dotnet add package Rheo.Sdk

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
});
PropertyTypeDefaultDescription
ApiKeystringrequiredAPI key from the Rheo partner dashboard
WebhookSecretstring?nullwhsec_... secret for webhook signature verification
BaseUrlUrihttps://market.rheo.seOverride for staging or self-hosted
TimeoutTimeSpan30 sPer-request timeout
MaxRetriesint3Max retries on 429 / 5xx
PartnerAccountstring?nullReseller 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 DI
builder.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 UUID

UpsertAsync 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 SEK

Update 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 vehicle
var page = await rheo.Items.ListAsync(new ListItemsParams
{
ParentExternalId = "VIN-YV1CZ714581234567",
Status = RheoItemStatus.Active,
Limit = 100,
});
// Incremental sync — changes in last 24 hours
var updated = await rheo.Items.ListAsync(new ListItemsParams
{
UpdatedSince = DateTimeOffset.UtcNow.AddDays(-1),
});
// Cursor pagination
string? 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 first
foreach (var e in history.Events)
Console.WriteLine($"{e.CreatedAt:O} — {e.EventType}");

Vehicle hierarchy

// Register the donor vehicle as a container
await 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 batch
await 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 yards
await 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 enum
enum RheoItemStatus { Draft, Active, Sold, NotListed, Archived }
// Item type
enum RheoItemType { Item, Container }
// Domain objects (polymorphic on the "domain" discriminator)
abstract class DomainObject { string Domain } // slug accessor, not serialized
class 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.