Callbacks
Receive real-time notifications about transaction status changes.
Overview
PayInn sends HTTP POST requests to your callback URL whenever a transaction status changes. These callbacks contain the transaction details and a signature for verification.
Important
You may receive multiple callbacks for the same transaction (e.g., status changes from pending to processing to completed). Make sure to handle callbacks idempotently - process each unique status change only once.
Callback URL Configuration
Your callback URL is configured via the admin panel when you register with PayInn.
| Requirement | Description |
|---|---|
| Protocol | HTTPS only (SSL/TLS required) |
| Method | POST |
| Content-Type | application/json |
| Response | Return 2xx status code to acknowledge receipt |
| Timeout | 30 seconds |
Callback Headers
Each callback includes these headers for verification:
| Header | Description |
|---|---|
X-Signature | HMAC-SHA256 signature |
X-Timestamp | Unix timestamp in seconds |
Content-Type | application/json |
User-Agent | PayInn-Callback/1.0 |
Deposit Callback Payload
{
"transactionId": "TXN-abc123def456",
"processId": "ORDER-12345",
"type": "deposit",
"status": "completed",
"amount": 1000,
"currency": "TRY",
"completedAt": "2024-01-15T12:15:00.000Z",
"timestamp": 1705320900
}Deposit Callback Fields
| Field | Type | Description |
|---|---|---|
transactionId | string | PayInn transaction ID |
processId | string | Your transaction ID |
type | string | Transaction type: deposit or withdrawal |
status | string | Transaction status: completed or failed |
amount | number | Confirmed transaction amount (may differ from requested) |
currency | string | Currency code |
completedAt | string | Completion time (ISO 8601) - only for completed status |
failureReason | string | Reason for failure - only for failed status |
timestamp | number | Callback timestamp |
Withdrawal Callback Payload
{
"transactionId": "TXN-xyz789abc123",
"processId": "WITHDRAW-12345",
"type": "withdrawal",
"status": "completed",
"amount": 5000,
"currency": "TRY",
"completedAt": "2024-01-15T12:30:00.000Z",
"timestamp": 1705321800
}Signature Verification
Always verify the callback signature to ensure the request is from PayInn. The signature is calculated using HMAC-SHA256 with your Callback Secret.
signature = HMAC-SHA256(rawRequestBody, callbackSecret)Use Raw Request Body
Always use the raw HTTP request body string for signature verification. Do NOT parse the JSON and re-serialize it, as different languages/frameworks may format numbers differently (e.g., 550 vs 550.0), causing signature mismatches.
Node.js Verification Example
const crypto = require('crypto');
const express = require('express');
const app = express();
const CALLBACK_SECRET = 'your-callback-secret';
// Capture raw body for signature verification
app.use(express.json({
verify: (req, res, buf) => {
req.rawBody = buf.toString();
}
}));
// Callback endpoint
app.post('/webhook/payinn', (req, res) => {
const signature = req.headers['x-signature'];
// IMPORTANT: Use raw body for signature verification
// Do NOT use JSON.stringify(req.body) as it may change number formatting
const expectedSignature = crypto
.createHmac('sha256', CALLBACK_SECRET)
.update(req.rawBody)
.digest('hex');
if (signature !== expectedSignature) {
console.error('Invalid signature');
return res.status(401).json({ error: 'Invalid signature' });
}
// Use parsed body for processing
const payload = req.body;
console.log('Received callback:', payload);
switch (payload.status) {
case 'completed':
// Credit the user's balance
creditUserBalance(payload.processId, payload.amount);
break;
case 'failed':
// Handle failed transaction
handleFailedTransaction(payload.processId, payload.failureReason);
break;
}
// Acknowledge receipt
res.status(200).json({ received: true });
});
app.listen(3000);PHP Verification Example
<?php
$callbackSecret = 'your-callback-secret';
// Get headers and body
$signature = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
$payload = file_get_contents('php://input');
$data = json_decode($payload, true);
// Verify signature
$expectedSignature = hash_hmac('sha256', $payload, $callbackSecret);
if (!hash_equals($expectedSignature, $signature)) {
http_response_code(401);
echo json_encode(['error' => 'Invalid signature']);
exit;
}
// Process the callback
switch ($data['status']) {
case 'completed':
// Credit the user's balance
creditUserBalance($data['processId'], $data['amount']);
break;
case 'failed':
// Handle failed transaction
handleFailedTransaction($data['processId'], $data['failureReason'] ?? null);
break;
}
// Acknowledge receipt
http_response_code(200);
echo json_encode(['received' => true]);C# Verification Example
using Microsoft.AspNetCore.Mvc;
using System.Security.Cryptography;
using System.Text;
[ApiController]
[Route("webhook")]
public class PayInnCallbackController : ControllerBase
{
private const string CallbackSecret = "your-callback-secret";
[HttpPost("payinn")]
public async Task<IActionResult> HandleCallback()
{
// IMPORTANT: Read raw body for signature verification
// Do NOT use model binding as it may change JSON formatting
using var reader = new StreamReader(Request.Body);
var rawBody = await reader.ReadToEndAsync();
var signature = Request.Headers["X-Signature"].FirstOrDefault();
// Verify signature using raw body
var expectedSignature = ComputeHmacSha256(rawBody, CallbackSecret);
if (signature != expectedSignature)
{
return Unauthorized(new { error = "Invalid signature" });
}
// Parse body for processing
var payload = System.Text.Json.JsonSerializer.Deserialize<CallbackPayload>(rawBody);
switch (payload.Status)
{
case "completed":
// Credit the user's balance
await CreditUserBalance(payload.ProcessId, payload.Amount);
break;
case "failed":
// Handle failed transaction
await HandleFailedTransaction(payload.ProcessId, payload.FailureReason);
break;
}
return Ok(new { received = true });
}
private static string ComputeHmacSha256(string data, string secret)
{
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(data));
return Convert.ToHexString(hash).ToLower();
}
}Retry Policy
If your endpoint doesn't respond with a 2xx status code, PayInn will retry the callback:
| Attempt | Delay |
|---|---|
| 1st retry | 5 minutes |
| 2nd retry | 15 minutes |
| 3rd retry | 1 hour |
Best Practices
- Always verify the signature before processing
- Return 200 OK as quickly as possible
- Process the callback asynchronously if needed
- Handle duplicate callbacks idempotently
- Log all callbacks for debugging
Transaction Statuses
| Status | Description | Action Required |
|---|---|---|
completed | Transaction completed successfully | Credit user balance / mark order as paid |
failed | Transaction failed | Show error to user, allow retry |