Docs menu
SDKs

C# and .NET integration

Screen sign-ups from C# with HttpClient; the webhook receiver is an ASP.NET Core minimal API. These are the console's snippets; there is no NuGet package.

Last updated

Screen a sign-up

Call /siteverify with the browser token before you create the account, and fall back to /verify when there is no token. The constants at the top choose strict mode and fail-closed behaviour. See the quickstart for the browser script.

C#
// ASP.NET Core. inlineshieldToken is the betshield_token form field, httpContext the visitor's request.
// Calls https://api.inlineshield.io/api/v1/siteverify (and https://api.inlineshield.io/api/v1/verify when there is no token).
const bool REQUIRE_INLINESHIELD_TOKEN = false; // true: reject signups that arrive without a token (NO_TOKEN). While your account is over its monthly limit, signups arrive without a token and InlineShield answers QUOTA_EXCEEDED, which follows INLINESHIELD_FAIL_CLOSED even in strict mode.
const bool INLINESHIELD_FAIL_CLOSED = false;   // true: reject signups while InlineShield is unreachable
var http = new HttpClient { Timeout = TimeSpan.FromSeconds(3) }; // reuse one instance in production

// Returns the JSON of a 2xx answer. Throws HttpRequestException on transport error, timeout, any non-2xx
// (402 QUOTA_EXCEEDED, 429, 5xx, 400 ...) or QUOTA_EXCEEDED, JsonException on non-JSON
// (= InlineShield unavailable), and UnauthorizedAccessException on 401 (bad API key: always rejected).
async Task<JsonElement> InlineShieldPost(string path, object payload)
{
    var req = new HttpRequestMessage(HttpMethod.Post, "https://api.inlineshield.io" + path);
    req.Headers.Add("X-Api-Key", Environment.GetEnvironmentVariable("INLINESHIELD_API_KEY"));
    req.Content = JsonContent.Create(payload);
    var resp = await http.SendAsync(req);
    var code = (int)resp.StatusCode;
    if (code == 401) throw new UnauthorizedAccessException("InlineShield HTTP 401 (check INLINESHIELD_API_KEY)");
    if (code < 200 || code > 299) throw new HttpRequestException("InlineShield HTTP " + code);
    var json = await resp.Content.ReadFromJsonAsync<JsonElement>();
    if (Str(json, "error") == "QUOTA_EXCEEDED") throw new HttpRequestException("InlineShield QUOTA_EXCEEDED");
    return json;
}

static string? Str(JsonElement j, string name) =>
    j.ValueKind == JsonValueKind.Object && j.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.String
        ? v.GetString() : null;

// Switch on action: "allow" → create the account; "hold" → create it pending and store reviewId; "block" → reject the signup.
string action;
string? reviewId = null; // only set for "hold"
try
{
    var result = await InlineShieldPost("/api/v1/siteverify", new { token = inlineshieldToken, email = email });
    if (Str(result, "error") == "NO_TOKEN" && !REQUIRE_INLINESHIELD_TOKEN)
    {
        // No token (script blocked, timed out or missing): judge the signup server-side
        result = await InlineShieldPost("/api/v1/verify", new {
            email = email,
            ip = httpContext.Connection.RemoteIpAddress?.ToString(),
            userAgent = httpContext.Request.Headers.UserAgent.ToString()
        });
        if (Str(result, "decision") == null) throw new HttpRequestException("InlineShield /verify answered without a decision");
    }
    else if (!(result.ValueKind == JsonValueKind.Object
            && result.TryGetProperty("success", out var ok) && ok.ValueKind == JsonValueKind.True))
    {
        result = JsonSerializer.SerializeToElement(new { action = "block" });
    }
    // Fail-open answers have no "action": fall back to the decision.
    action = Str(result, "action") ?? (Str(result, "decision") switch { "BLOCKED" => "block", "QUARANTINED" => "hold", _ => "allow" });
    reviewId = Str(result, "reviewId");
}
catch (UnauthorizedAccessException e)
{
    logger.LogError(e, "InlineShield rejected the request");
    action = "block";
}
catch (Exception e) when (e is HttpRequestException || e is TaskCanceledException || e is JsonException)
{
    logger.LogWarning(e, "InlineShield unavailable");
    action = INLINESHIELD_FAIL_CLOSED ? "block" : "allow";
}
if (action == "block")
    return Results.StatusCode(403);
if (action == "hold")
{
    // Create the account as pending (no login, no welcome email) and save reviewId with it.
}
else
{
    // "allow": create the account normally.
}

Verify webhooks

Check the signature of each delivery before you trust it. See Webhooks.

C#
// ASP.NET Core minimal API. X-InlineShield-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">
app.MapPost("/inlineshield/webhook", async (HttpRequest request) =>
{
    using var ms = new MemoryStream();
    await request.Body.CopyToAsync(ms);
    var raw = ms.ToArray(); // raw bytes, before any JSON parsing

    var m = Regex.Match(request.Headers["X-InlineShield-Signature"].ToString(), @"\At=(\d{1,12}),v1=([0-9a-f]{64})\z");
    if (!m.Success || Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - long.Parse(m.Groups[1].Value)) > 300)
        return Results.BadRequest(); // malformed header or older/newer than 300 s

    var secret = Environment.GetEnvironmentVariable("INLINESHIELD_WEBHOOK_SECRET");
    if (string.IsNullOrEmpty(secret)) return Results.StatusCode(500); // never verify with an empty key
    var key = Encoding.UTF8.GetBytes(secret);
    var signed = Encoding.UTF8.GetBytes(m.Groups[1].Value + ".").Concat(raw).ToArray();
    var expected = HMACSHA256.HashData(key, signed);
    if (!CryptographicOperations.FixedTimeEquals(expected, Convert.FromHexString(m.Groups[2].Value))) // constant-time
        return Results.BadRequest();

    using var doc = JsonDocument.Parse(raw);
    var evt = doc.RootElement;
    var d = evt.GetProperty("data");
    switch (evt.GetProperty("event").GetString())
    {
        case "signup.approved":
            // Activate the account that has reviewId d.GetProperty("reviewId") (status "approved").
            break;
        case "signup.rejected":
            // Delete or disable it (see d.GetProperty("decisionReason")).
            break;
    }
    // Deliveries can repeat: make the handler idempotent (key on evt.GetProperty("id")).
    return Results.Ok();
});

Review status

The fallback to webhooks for held sign-ups. See Hold and review.

C#
// Response: { reviewId, status: "pending" | "approved" | "rejected", decisionReason, decidedAt, expiresAt }. Prefer the webhook; poll only as a fallback (limit 60 requests/min per key). 404 = unknown review.
using var req = new HttpRequestMessage(HttpMethod.Get, "https://api.inlineshield.io/api/v1/reviews/" + Uri.EscapeDataString(reviewId));
req.Headers.Add("X-Api-Key", Environment.GetEnvironmentVariable("INLINESHIELD_API_KEY"));
var resp = await http.SendAsync(req); // reuse one HttpClient with a 3 s timeout
if (resp.IsSuccessStatusCode)
{
    var review = await resp.Content.ReadFromJsonAsync<JsonElement>();
    var status = review.GetProperty("status").GetString(); // "pending" | "approved" | "rejected"
}

Outcome feedback

Report fraud or legit outcomes by requestId. See Outcome feedback.

C#
// Tell InlineShield how a signup really turned out (fraud or legit), for example after a chargeback, a failed or passed KYC, or bonus abuse, so detection learns from your outcomes. Identify the signup by the requestId InlineShield returned (or send token: the /check token instead). Optional: category (bonus_abuse, multi_account, chargeback, kyc_failed, bot, good_customer, other) and note (up to 200 characters, never shown outside your account). Response 200 { success, outcome, category, changed }; sending the same feedback again changes nothing. 404 = unknown requestId, 422 = the signup is older than 180 days. Not counted toward your monthly limit; limited per key like /verify.
using var req = new HttpRequestMessage(HttpMethod.Post, "https://api.inlineshield.io/api/v1/feedback")
{
    Content = JsonContent.Create(new { requestId, outcome = "fraud", category = "chargeback" })
};
req.Headers.Add("X-Api-Key", Environment.GetEnvironmentVariable("INLINESHIELD_API_KEY"));
var resp = await http.SendAsync(req); // reuse one HttpClient with a 3 s timeout
if (!resp.IsSuccessStatusCode)
{
    Console.Error.WriteLine($"InlineShield feedback not recorded: {(int)resp.StatusCode}");
}

Errors

The snippets treat a 401 as a configuration error (reject) and every other failure, including 402 QUOTA_EXCEEDED, 429, 5xx and timeouts, as "InlineShield unavailable" (accept and log, unless you choose fail-closed). Every code is on Errors and HTTP codes.