Benten blog · Tutorial

Add phone approval to any action in ASP.NET Core in 10 minutes

Before your API moves money, changes a password or deletes data, ask the account owner on their phone, and get the answer back in the same request.

All posts
The Benten team2 November 20268 min read

Some actions in your app deserve a second check: a large transfer, a new payout bank account, an admin deleting customer data, a smart lock being opened remotely. A valid session isn't enough proof that the person behind it really wants this to happen. Stolen cookies, hijacked sessions and over-eager scripts all look like a signed-in user.

In this tutorial we'll add an "Is this really you?" step to an ASP.NET Core endpoint. Before the endpoint runs, the account owner gets a request on their phone showing exactly what they're approving, taps Accept or Reject, and your code gets the answer back in the same HTTP request.

Why not SMS codes or an authenticator app?

ApproachWhat the user doesThe catch
SMS one-time codeTypes a 6-digit codeCosts money per message, can be delayed, and is exposed to SIM swaps and phishing
Authenticator app (TOTP)Opens an app, types a codeThe code says nothing about what is being approved, and it can be phished
Build your own push approvalTaps whatever you buildWeeks of work: a mobile app, FCM and APNs, device registration, timeouts, replay protection
BentenReads the details, taps Accept or RejectThe user installs the free Benten App and registers their phone once

Benten does the last row for you: a hosted service, a free app for your users on iOS and Android, and an MIT-licensed SDK. Your server makes one call.

What we'll build

A minimal API with one endpoint, POST /transfers. Before it "sends" a wire transfer, it asks the account owner to approve the amount and payee on their phone:

  • Accept: the transfer goes ahead (200).
  • Reject, or no answer within 60 seconds: the transfer is refused (403).
  • Anything else, such as a phone that isn't registered, is reported as an error with a Benten status code.

You'll need the .NET 8 SDK or later, and a phone with the free Benten App (App Store, Google Play) with your phone number registered in it.

Step 1: Get an API key

Create a developer account on the Developer Portal. It's free during early access, with no credit card needed. After you confirm your email, we call to verify your phone number within one business day and activate the account. Then sign in and create an API key on the dashboard. It starts with bnt_sk_ and is shown only once.

Step 2: Create the project and add the SDK

Terminal
dotnet new web -n TransferApi
cd TransferApi
dotnet add package Benten.SDK

Keep the API key out of your code and out of source control. For local development, use user secrets. We'll also store the phone number to send the test request to:

Terminal
dotnet user-secrets init
dotnet user-secrets set "Benten:ApiKey" "bnt_sk_your-api-key"
dotnet user-secrets set "Demo:PhoneNumber" "6501234567"

In production, put the key in an environment variable or a key vault. Never ship it in a mobile app or browser JavaScript: approval requests always come from your server.

Step 3: Ask for approval before the transfer

Replace Program.cs with this:

Program.cs
using System.Globalization;
using Benten.SDK;
using Benten.SDK.Models;

var builder = WebApplication.CreateBuilder(args);

// One BentenClient for the whole app: it caches and renews its access token,
// and is safe to use from many requests at once.
builder.Services.AddSingleton(
    new BentenClient(builder.Configuration["Benten:ApiKey"]
        ?? throw new InvalidOperationException("Set Benten:ApiKey with dotnet user-secrets.")));

var app = builder.Build();

app.MapPost("/transfers", async (TransferRequest transfer, BentenClient benten,
                                 IConfiguration config, CancellationToken ct) =>
{
    // In a real app, look up the signed-in user's own phone number from your
    // user store. Never take it from the request body.
    string phone = config["Demo:PhoneNumber"]!;

    BentenResult result;
    try
    {
        result = await benten.RequestGenericNotificationApprovalAsync(new GenericNotificationRequest
        {
            Country = "United States",
            PhoneNumber = phone,
            NotificationSubType = "Wire transfer",
            Line1Placeholder = "Amount",
            Line1 = transfer.Amount.ToString("C", CultureInfo.GetCultureInfo("en-US")),
            Line2Placeholder = "To",
            Line2 = transfer.Payee
        }, ct);
    }
    catch (BentenException ex)
    {
        // The API key was rejected, or Benten couldn't be reached
        return Results.Problem($"Approval service unavailable: {ex.Message}", statusCode: 503);
    }

    return result.Outcome switch
    {
        ApprovalOutcome.Approved => Results.Ok(new { status = "sent" }),   // make the transfer here
        ApprovalOutcome.Denied   => Results.Problem("The account owner rejected this transfer.", statusCode: 403),
        ApprovalOutcome.TimedOut => Results.Problem("The transfer wasn't approved within 60 seconds.", statusCode: 403),
        _                        => Results.Problem($"Approval failed: {result.Error}", statusCode: 502)
    };
});

app.Run();

record TransferRequest(decimal Amount, string Payee);

A few things worth noticing:

  • The labelled lines are what the user sees. The phone shows "Wire transfer", then Amount: $2,000.00 and To: Acme Supplies. Up to three label/value lines fit, so put in the details someone needs to recognise a request they didn't make.
  • Only an explicit Accept counts. Outcome starts as Error, so a dropped connection or an unexpected reply can never be mistaken for approval.
  • Errors come back as data, not exceptions. If the phone number has no Benten App registration, the result carries a status code such as BSC4022 with a readable message. BentenException is only for problems reaching Benten or a rejected API key.
  • The request waits for the person. The call returns as soon as they answer, or after 60 seconds. Show an "Approve on your phone" message in your UI while it waits.

Step 4: Try it

Terminal
dotnet run
# in a second terminal (use the port dotnet run prints)
curl -X POST http://localhost:5000/transfers -H "Content-Type: application/json" -d "{\"amount\": 2000, \"payee\": \"Acme Supplies\"}"

Your phone shows the request with the amount and payee. Tap Accept and curl prints {"status":"sent"}. Run it again and tap Reject, or wait a minute, and you get a 403 instead.

What happens under the hood

  1. The SDK exchanges your API key for a short-lived access token (one hour), and renews it before it expires.
  2. It opens a WebSocket to the Benten API and sends the request.
  3. Benten looks up the device registered for that phone number and sends it a push notification.
  4. The user's answer comes back on the same WebSocket. Only the device the request was sent to can answer it, each request gets exactly one answer, and late answers are rejected.
  5. If nobody answers within 60 seconds, you get TimedOut.

Beyond the generic request

The generic request in this tutorial needs only the user's phone number, which makes it the quickest place to start. The SDK has three more request types, with screens designed for them:

  • RequestLoginApprovalAsync: approve a sign-in to a registered account.
  • RequestCCTransactionApprovalAsync: approve a card payment, showing the retailer, amount and card.
  • RequestIoTSettingApprovalAsync: approve a change to a registered device, like a thermostat or a lock.

These need the user's account, card or device registered with Benten first. The Developer Guide covers registration and every status code.

Not using .NET?

Benten is plain HTTPS and WebSockets with JSON, so any language works. There's a ready-made Python client (one file, one dependency), and the Developer Guide documents every endpoint.

Limits worth knowing

  • Your users install the free Benten App and register their phone number once.
  • There's no sandbox with test phone numbers yet, so you test with a real phone.
  • New developer accounts are activated after a short phone verification.

Try it before you write any code: the live demo sends a sign-in, a card payment, a thermostat change and a custom request to your own phone.

Links

Questions, or something that didn't work as described? Open an issue on GitHub or email support@bententechnologies.com. We read everything.
— The Benten team