Docs menu
Guides
API reference
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.
// 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.
// 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.
// 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.
// 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.