Telr payments in ASP.NET Core: the parts the docs skip
A ticketing team's account of wiring Telr's hosted payment page into ASP.NET Core, where the hard part is not the redirect but everything after it: a return URL that proves nothing, a signed callback that can arrive twice or late, and a server-side order/check that should be the only source of truth. Covers one session per order, invariant-culture amounts, callback signature checks and single-claim fulfilment.
Key points
- Only order/check should change order state; the return URL is user-facing and the callback counts as evidence after signature validation.
- Lock the order row so a double click, retry or second tab cannot open two Telr sessions for the same order.
- Amounts travel as strings: format with CultureInfo.InvariantCulture, round tax per line with MidpointRounding.AwayFromZero, keep the total as decimal.
- Verify store id, test flag, currency and the SHA1 advice hash in constant time, then persist the result with a single conditional update.
- Use ExecuteUpdateAsync filtered on Pending/OnHold so only one handler fulfils, and move sold counts with an atomic SQL increment.
- Reconcile expired 30-minute reservations with order/check every five minutes instead of cancelling them outright.
Telr's hosted payment page keeps card data out of your API: create a session, redirect the customer, then wait. Anas Amin's article covers what the vendor docs skip. Three signals report the outcome, but only one is authoritative: the browser return URL is for users, the signed callback is evidence once validated, and order/check — a server-side lookup by reference — is what should change state.
The details carry the weight: a locked order row and one hosted session, no retry of a gateway call whose outcome is unknown, invariant-culture amounts built from rounded decimal lines, a constant-time SHA1 signature check, and a conditional EF Core update so the callback and the browser's verify call can settle an order only once. Expired reservations are reconciled with order/check before seats are released.
Real-world use
- A ticketing platform holding seats for 30 minutes where a payment can still arrive after the reservation expired.
- A checkout used by an Angular site and a Flutter WebView, where the mobile app must never read success from the URL it was sent to.
- A team auditing an existing payment integration after duplicate 'paid' notifications or seats sold twice.
- A CI/CD deploy script that must fail when production still carries test-mode gateway settings.
Why it matters
Idempotency and server-side verification are the difference between a customer who paid once for one seat and a support queue full of double charges, oversold events and manual refunds.