Webhook verification
When a transaction occurs, Tingee sends a Webhook (IPN) to the partner's endpoint. The SDK provides the verifyWebhookSignature() method to verify the signature simply and securely.
Why verify?
The x-signature header ensures the request comes from Tingee and not from a third party impersonating it. Always verify before processing the transaction.
Usage
- Node.js (Express)
- Node.js (NestJS)
- Java (Spring Boot)
- C# (ASP.NET Core)
- PHP (plain)
- PHP (Laravel)
import express from 'express';
import { TingeeClient, isErrorResponse } from '@tingee/sdk-node';
const client = new TingeeClient({
secretKey: process.env.TINGEE_SECRET_KEY!,
clientId: process.env.TINGEE_CLIENT_ID!,
});
app.post('/webhook/tingee', express.json(), (req, res) => {
const result = client.verifyWebhookSignature(
req.headers['x-signature'] as string,
req.headers['x-request-timestamp'] as string,
req.body,
);
if (isErrorResponse(result)) {
return res.status(401).json({ error: result.message });
}
// ✓ Valid signature — process the transaction
const { transactionCode, amount } = req.body;
// Update the order...
res.json({ received: true });
});
import { Controller, Post, Req, Body, UnauthorizedException } from '@nestjs/common';
import { TingeeClient, isErrorResponse } from '@tingee/sdk-node';
import { Request } from 'express';
@Controller()
export class WebhookController {
constructor(private readonly tingeeClient: TingeeClient) {}
@Post('webhook/tingee')
handleWebhook(@Req() req: Request, @Body() body: Record<string, any>) {
const result = this.tingeeClient.verifyWebhookSignature(
req.headers['x-signature'] as string,
req.headers['x-request-timestamp'] as string,
body,
);
if (isErrorResponse(result)) {
throw new UnauthorizedException(result.message);
}
// ✓ Process the transaction...
return { received: true };
}
}
import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;
import vn.tingee.sdk.TingeeClient;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Map;
@RestController
public class WebhookController {
private final TingeeClient client;
public WebhookController(TingeeClient client) {
this.client = client;
}
@PostMapping("/webhook/tingee")
public ResponseEntity<?> handleWebhook(
HttpServletRequest req,
@RequestBody Map<String, Object> body) {
var result = client.verifyWebhookSignature(
req.getHeader("x-signature"),
req.getHeader("x-request-timestamp"),
body
);
if (!result.isError()) {
return ResponseEntity.status(401)
.body(Map.of("error", result.getMessage()));
}
// ✓ Valid signature — process the transaction
return ResponseEntity.ok(Map.of("received", true));
}
}
using Microsoft.AspNetCore.Mvc;
using Tingee.Sdk.Client;
[ApiController]
public class WebhookController : ControllerBase
{
private readonly TingeeClient _client;
public WebhookController(TingeeClient client) => _client = client;
[HttpPost("/webhook/tingee")]
public async Task<IActionResult> HandleWebhook(
[FromBody] object body)
{
var signature = Request.Headers["x-signature"].ToString();
var timestamp = Request.Headers["x-request-timestamp"].ToString();
var result = _client.VerifyWebhookSignature(signature, timestamp, body);
if (!result.IsValid)
return Unauthorized(new { error = result.Message });
// ✓ Valid signature — process the transaction
return Ok(new { received = true });
}
}
<?php
$signature = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
$timestamp = $_SERVER['HTTP_X_REQUEST_TIMESTAMP'] ?? '';
$body = json_decode(file_get_contents('php://input'), true);
$result = $client->verifyWebhookSignature($signature, $timestamp, $body);
if ($result->isError()) {
http_response_code(401);
echo json_encode(['error' => $result->message]);
exit;
}
// ✓ Valid signature — process the transaction
echo json_encode(['received' => true]);
use Illuminate\Http\Request;
use Tingee\Sdk\TingeeClient;
Route::post('/webhook/tingee', function (Request $request, TingeeClient $client) {
$result = $client->verifyWebhookSignature(
$request->header('x-signature'),
$request->header('x-request-timestamp'),
$request->all()
);
if ($result->isError()) {
abort(401, $result->message);
}
// ✓ Process the transaction...
return response()->json(['received' => true]);
});
Tingee retry policy
If the partner's endpoint does not respond within 10 seconds (timeout), Tingee automatically resends the webhook.
Idempotency
Always use transactionId or orderId to check whether the order has already been processed, to avoid duplicates when you receive a retry.
See also
- Webhook (IPN) overview — Flow and event catalog
- Webhook Payment Callback — Detailed payload structure