Receive success and failure notifications for collections, disbursements, and refunds.
Overview
Register a public HTTPS endpoint in the TricsoftPay user dashboard, verify every notification against its raw request body, and process repeated deliveries idempotently.
Receive transaction events
Transaction types and statuses
TricsoftPay currently sends webhooks for three transaction types.
Supported transaction types
Transaction type
Description
collection
A payment collected from a customer.
disbursement
Funds sent to a recipient.
refund
Funds returned for an eligible payment.
Webhook notifications currently contain only terminal statuses. Validate the exact lowercase values shown below.
Terminal webhook statuses
Status
Meaning
success
The transaction completed successfully.
failed
The transaction reached a failed terminal state.
Use success and failed when validating data.status. Do not substitute completed or successful.
Register a public endpoint
Create a public HTTPS endpoint in your application, then register its complete URL in the TricsoftPay user dashboard. The receiver belongs to your application, so examples use {{webhook_url}} rather than the TricsoftPay API {{base_url}}.
Endpoint requirements
Use HTTPS.
Make the endpoint publicly reachable from the internet.
Accept POST requests with an application/json body.
Do not protect the route with user-session or API-key authentication; authenticate each request with its webhook signature.
Public webhook route
import express from "express";
const baseRouter = express.Router();
baseRouter.post(
"/webhooks",
express.raw({ type: "application/json" }),
webhookController.handleWebhook,
);
// Register {{webhook_url}}/webhooks in the TricsoftPay user dashboard.
http.HandleFunc("/webhooks", webhookHandler)
// Register {{webhook_url}}/webhooks in the TricsoftPay user dashboard.
func webhookHandler(w http.ResponseWriter, r *http.Request) {
rawBody, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "invalid request body", http.StatusBadRequest)
return
}
_ = rawBody // Verify before decoding the JSON payload.
}
let app = Router::new().route(
"/webhooks",
post(webhook_handler),
);
// Register {{webhook_url}}/webhooks in the TricsoftPay user dashboard.
async fn webhook_handler(headers: HeaderMap, body: Bytes) -> StatusCode {
// Verify body.as_ref() before decoding the JSON payload.
StatusCode::OK
}
@RestController
class WebhookController {
// Register {{webhook_url}}/webhooks in the TricsoftPay user dashboard.
@PostMapping(
value = "/webhooks",
consumes = MediaType.APPLICATION_JSON_VALUE
)
ResponseEntity<?> receiveWebhook(
@RequestHeader("x-webhook-signature") String signature,
@RequestBody byte[] rawBody
) {
// Verify rawBody before decoding the JSON payload.
return ResponseEntity.ok(Map.of("received", true));
}
}
app.MapPost("/webhooks", async (HttpRequest request) =>
{
using var stream = new MemoryStream();
await request.Body.CopyToAsync(stream);
var rawBody = stream.ToArray();
// Verify rawBody before decoding the JSON payload.
return Results.Ok(new { received = true });
});
// Register {{webhook_url}}/webhooks in the TricsoftPay user dashboard.
Register in the dashboard
1
Open Developers
Sign in to the user dashboard and open the Developers section.
2
Select Webhooks
Open the Webhooks page under Developers.
3
Enter the endpoint
Provide the complete public URL, such as {{webhook_url}}/webhooks.
4
Save the webhook
Select Save Webhook and confirm that the current webhook URL is updated.
5
Configure the secret
Obtain the webhook secret from the platform and store it securely as WEBHOOK_SECRET. More detailed retrieval and rotation instructions are awaiting confirmation.
Verify the signature
TricsoftPay signs the exact raw request body with HMAC-SHA256 and sends the result in x-webhook-signature. Obtain the webhook secret from the platform and keep it outside your source code.
The signature header encoding and any prefix are awaiting confirmation. Keep signature decoding isolated so it can be updated without changing request handling.
HMAC verification
import { createHmac, timingSafeEqual } from "node:crypto";
const webhookSecret = process.env.WEBHOOK_SECRET;
function decodeWebhookSignature(value: string): Buffer {
// Confirm the header encoding and optional prefix with TricsoftPay.
// Replace this placeholder when the signature format is published.
throw new Error(`Webhook signature decoding is not configured: ${value}`);
}
function verifySignature(rawBody: Buffer, signature: string): boolean {
if (!webhookSecret) {
throw new Error("WEBHOOK_SECRET is not configured");
}
const expected = createHmac("sha256", webhookSecret)
.update(rawBody)
.digest();
const received = decodeWebhookSignature(signature);
return (
expected.length === received.length &&
timingSafeEqual(expected, received)
);
}
func decodeWebhookSignature(value string) ([]byte, error) {
// Confirm the encoding and optional prefix with TricsoftPay.
return nil, errors.New("webhook signature decoding is not configured")
}
func verifySignature(rawBody []byte, signature string) (bool, error) {
secret := os.Getenv("WEBHOOK_SECRET")
if secret == "" {
return false, errors.New("WEBHOOK_SECRET is not configured")
}
received, err := decodeWebhookSignature(signature)
if err != nil {
return false, err
}
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(rawBody)
return hmac.Equal(mac.Sum(nil), received), nil
}
use hmac::{Hmac, Mac};
use sha2::Sha256;
type HmacSha256 = Hmac<Sha256>;
fn decode_webhook_signature(_value: &str) -> Result<Vec<u8>, String> {
// Confirm the encoding and optional prefix with TricsoftPay.
Err("webhook signature decoding is not configured".into())
}
fn verify_signature(raw_body: &[u8], signature: &str) -> Result<bool, String> {
let secret = std::env::var("WEBHOOK_SECRET")
.map_err(|_| "WEBHOOK_SECRET is not configured")?;
let received = decode_webhook_signature(signature)?;
let mut mac = HmacSha256::new_from_slice(secret.as_bytes())
.map_err(|_| "invalid webhook secret")?;
mac.update(raw_body);
Ok(mac.verify_slice(&received).is_ok())
}
static byte[] decodeWebhookSignature(String value) {
// Confirm the encoding and optional prefix with TricsoftPay.
throw new IllegalStateException(
"Webhook signature decoding is not configured"
);
}
static boolean verifySignature(byte[] rawBody, String signature)
throws GeneralSecurityException {
String secret = System.getenv("WEBHOOK_SECRET");
if (secret == null || secret.isBlank()) {
throw new IllegalStateException("WEBHOOK_SECRET is not configured");
}
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(
secret.getBytes(StandardCharsets.UTF_8),
"HmacSHA256"
));
byte[] expected = mac.doFinal(rawBody);
byte[] received = decodeWebhookSignature(signature);
return MessageDigest.isEqual(expected, received);
}
static byte[] DecodeWebhookSignature(string value)
{
// Confirm the encoding and optional prefix with TricsoftPay.
throw new InvalidOperationException(
"Webhook signature decoding is not configured"
);
}
static bool VerifySignature(byte[] rawBody, string signature)
{
var secret = Environment.GetEnvironmentVariable("WEBHOOK_SECRET");
if (string.IsNullOrWhiteSpace(secret))
throw new InvalidOperationException("WEBHOOK_SECRET is not configured");
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var expected = hmac.ComputeHash(rawBody);
var received = DecodeWebhookSignature(signature);
return CryptographicOperations.FixedTimeEquals(expected, received);
}
Webhook payload
The complete webhook body is awaiting confirmation. The handler currently relies on data.external_id and data.status.
Current payload fields
Field
Type
Description
data.external_id
string
The external_id supplied when the transaction was created. Retries preserve this value.
data.status
string
The terminal webhook status. Currently success or failed.
type WebhookPayload struct {
Data WebhookData `json:"data"`
}
type WebhookData struct {
ExternalID string `json:"external_id"`
Status string `json:"status"`
}
record WebhookPayload(WebhookData data) {}
record WebhookData(
@JsonProperty("external_id") String externalId,
String status
) {}
public sealed record WebhookPayload(WebhookData Data);
public sealed record WebhookData(
[property: JsonPropertyName("external_id")] string ExternalId,
string Status
);
Process events safely
1
Read the raw body
Preserve the exact request bytes before JSON parsing or other body transformations.
2
Verify the signature
Reject missing or invalid x-webhook-signature values before trusting the payload.
3
Parse and validate
Decode JSON only after verification and require data.external_id with a supported terminal status.
4
Check idempotency
Use the stable external_id and terminal status to prevent a retried notification from applying the same business operation twice.
5
Apply the update
Run your application-specific transaction update atomically and record that the notification was processed.
6
Acknowledge receipt
Return HTTP 200 with received set to true after processing succeeds. Return a non-2xx response when the notification cannot be accepted.
Webhook responses
HTTP status
When to use it
200
The signed notification was accepted, including a duplicate that was already processed.
400
The signed request contains an invalid or unsupported payload.
401
The signature header is missing or the signature is invalid.
500
A temporary application failure prevented the notification from being processed.
Generic webhook handler
Implement business processing behind a small idempotent service boundary. The receiver should not contain application-specific accounting or fundraiser logic.