.NET SDK
Anis.Partners — for .NET 8 and .NET 10.
- Package: nuget.org/packages/Anis.Partners
- Source code: github.com/anis-ly/anis.partners-sdk-dotnet
An optional helper, provided as is
Every Anis SDK is provided free of charge under the MIT License, “as is”, without warranty of any kind. Anis (Aniscom for Technical Services) accepts no responsibility or liability for its use or for any loss arising from it. You remain responsible for your own integration — recording orders before you send them, recovery, key custody and testing. The source code is public: read it to understand exactly what it does before you rely on it. You do not need an SDK — you can call the Anis Partner API directly using these guides and the API reference.
SDK 2.2.0. SDK 1.x is deprecated; see the changelog.
dotnet add package Anis.Partners --version 2.2.0
Register it
builder.Services
.AddAnisPartners(builder.Configuration.GetSection("AnisPartners"))
.WithSigner(EcdsaP256Signer.FromPemFile("/secure/partner-key.pem", keyId));
{
"AnisPartners": {
"Authority": "https://<the address Anis gave you>",
"SignatureLifetime": "00:01:00",
"AcceptLanguage": "English",
"SigningKeyCacheDuration": "00:05:00",
"Timeout": "00:00:30"
}
}
Then take IAnisPartnersClient from the container. The settings are checked when your application starts: a
missing address, a signature lifetime outside 1 to 60 seconds, or a key cache longer than 5 minutes stops it there.
The signer is a separate step on purpose — there is no safe default for where your private key lives.
EcdsaP256Signer signs with a key file; for a vault or hardware module, implement IRequestSigner — your key id,
and a method that signs bytes and returns the 64-byte signature — and the key never enters your process.
Use it
var profile = await anis.Profile.GetAsync(ct);
await foreach (var wallet in anis.Wallets.ListAsync(ct)) // pages followed for you
Console.WriteLine($"{wallet.Name} {wallet.Balance}");
await foreach (var card in anis.Catalogue.ListCardsAsync(walletId, subcategoryId, ct)) // every page, followed for you
Console.WriteLine($"{card.Name?.En} {card.UnitPrice}");
var credential = await anis.OwnedCards.RevealAsync(walletId, soldCardId, ct);
What it verifies
SDK 2.0.1 and later verify every answer of the calls Anis signs — place an order, read an order, reveal a
card, reveal an invoice’s cards, the four enrolment calls, application suspension, key revocation and the signature self-check — success and every refusal.
It requires the signature there: an answer on one of these calls without a valid signature is thrown away
(UnverifiableResponseException) and, on an order, comes back as OrderOutcomeUnknown. It does not verify the information calls — your
profile, wallets, the catalogue and your owned cards (list and read): their answers carry no signature, and TLS protects
them. It still signs every request. See Verifying answers for which answers are signed.
Orders
var operationId = Guid.NewGuid(); // yours: store it with the request BEFORE sending
var outcome = await anis.Orders.CreateAsync(walletId, operationId, new CreateOrderRequest
{
CardId = card.Id,
Quantity = 2,
ExpectedUnitPrice = card.UnitPrice!.Value,
ExpectedTotal = card.UnitPrice!.Value.Multiply(2), // exact decimal
}, ct);
await (outcome switch
{
OrderCompleted c when c.CodesWithheld => ReportCodesWithheldAsync(operationId, ct), // paid, codes withheld: do not buy again; write to support@anis.ly
OrderCompleted c => vault.StoreAsync(c.Credentials, ct), // store FIRST
OrderReplayed r => EnsureStoredAsync(r.Order, ct), // sent earlier: if you never stored them, reveal by r.Order.InvoiceId
OrderProcessing p => ResumeAfterAsync(operationId, p.RetryAfter), // may still complete
OrderOutcomeUnknown u => ResumeAfterAsync(operationId, u.SuggestedDelay), // timed out, connection lost, an untrusted answer, a rate limit, or an access refusal
OrderNotPlaced n => CloseAsNotPlacedAsync(operationId, n.Refusal), // nothing bought: fix the cause, new order, new id
_ => throw new InvalidOperationException($"Unhandled order outcome {outcome}"),
});
ResumeAsync sends the stored order again under the same id and returns the same five outcomes.
A rate-limited CreateAsync returns OrderOutcomeUnknown, with the signed Retry-After as SuggestedDelay.
Access refusals (invalid_credentials, insufficient_scope, wallet_not_granted) also return
OrderOutcomeUnknown, with a suggested delay of 60 seconds: restore access, then resume the same id.
Refusals, timeouts and unverifiable answers return outcomes. Invalid requests, including a unit price that is
not greater than zero (ArgumentOutOfRangeException, before sending), signer failures and your cancellation
can throw. See Orders and recovery for the full rules.
OrderCompleted.CodesWithheld is true when the order is paid and complete but its codes were withheld: do not buy
it again. A reveal of the withheld cards is refused; the other cards of the order can be revealed. Write to
support@anis.ly with the operation id.
SDK 2.0.1 and later use CardSummary inside owned cards, reveals and order soldCards. Owned cards carry
SoldCardId and OperationId; face value, logo, subcategory and category are inside Card. A revealed
credential has the same full shape inside an order as in a reveal. Wallet currency is Balance.Currency.
Refusals
An order does not throw a refusal: it comes back as OrderNotPlaced (in Refusal) or, when the order may still
complete or an earlier attempt may have, as OrderOutcomeUnknown (in Cause). Other SDK calls throw refusals. The refusal is an AnisApiException
(or a subclass for the cases you handle differently) carrying Code, RequestId, RetryAfter and IsReplayed.
Each error page names its type.
Three other exceptions mean something different:
| Exception | Means | On an order |
|---|---|---|
UnverifiableResponseException |
An answer on a signed call could not be shown to come from Anis; it was thrown away | Not thrown — the order comes back as OrderOutcomeUnknown: resume with the same id |
LimitExceededException (spending_limit_exceeded, 429) |
The order would pass the application’s or wallet’s spending cap for the current window; nothing was bought or charged | Not thrown — OrderNotPlaced. After RetryAfter, place a new order with a new id; never resume this one |
RequestSigningException |
Your signer failed; nothing was sent | Thrown — send it again once the signer works |
TaskCanceledException, HttpRequestException |
Timed out, or the connection failed | Not thrown — OrderOutcomeUnknown: resume with the same id. Only your own cancellation is thrown |
Several Anis applications in one host
builder.Services.AddAnisPartners("brand-a", config.GetSection("AnisPartners:BrandA")).WithSigner(signerA);
builder.Services.AddAnisPartners("brand-b", config.GetSection("AnisPartners:BrandB")).WithSigner(signerB);
var brandA = factory.GetClient("brand-a"); // IAnisPartnersClientFactory, or [FromKeyedServices("brand-a")]
Each application has its own pipeline and key, and its traces and metrics carry anis.client.
No automatic retries on this client
A retry policy added to every HTTP client (for example the .NET Aspire service defaults) gives up on each attempt
after 10 seconds by default, sooner than the SDK’s 30. A slow order can then complete at Anis while its answer —
with the card codes — is thrown away. Retry reads yourself if you like; recover orders with ResumeAsync.
Observability
Traces and metrics under Anis.Partners.Sdk; logs through your own logging. See
Observability for the metric names and what to alert on.
If your host replaces .NET’s built-in container
The SDK uses .NET 8 keyed services. A third-party container must support them — Autofac 9 or later, Lamar 12.1 or
later, and SimpleInjector all do. Otherwise build the client without the container: AnisPartnersClient.Create(options, signer) gives you the same signed and verified client. Build it once and keep it.
Methods and card answers
Owned-card calls use /v1/wallets/{walletId}/owned-cards, its /{soldCardId} read and
/{soldCardId}/reveal, and /v1/wallets/{walletId}/invoices/{invoiceId}/owned-cards/reveal.
Catalogue card reads use GET /v1/wallets/{walletId}/catalog/cards/{cardId}.
CardSummary is shared by owned cards, reveals and order soldCards: card id and localized name,
optional face value and logo, subcategory and category. Owned cards use soldCardId and operationId.
Revealed credentials inside orders include invoice id, card summary and purchase time too. Empty strings
and empty localized text are omitted. Money is a string with three decimals and an uppercase three-letter
currency — negative only for a wallet balance while the wallet uses allowed debt; read a wallet’s currency from balance.currency.
The SDK requires the v2 signing tag, both request signature headers in the covered list, and the key-document lifecycle checks. See Verifying answers. Stops are immediate; only Anis staff can resume an application, and revoking your current key requires new enrolment. See Security.
var reason = "Suspected compromise";
var catalogueCard = await client.Catalogue.GetCardAsync(walletId, cardId);
var suspended = await client.Access.SuspendApplicationAsync(reason);
var revoked = await client.Access.RevokeKeyAsync(keyId, reason);
Product resources in SDK 2.2.0
SDK 2.2.0 adds meterCharges, directRechargeProducts (products, locations, quote), directRecharges (create, a list of your own, read and recipient updates), webhookEndpoint, webhookEvents, a webhook signature verifier and updated error types. Upgrade from 2.1.0 to use them. Follow the meter, Direct recharge and webhook guides for HTTP behavior. Keep the operation id and exact body before a money-moving call; a lost answer is recovered with the same id. Each SDK’s README and its docs/webhooks.md give the method names and the verifier’s arguments.