Transaction Callback
Handling Transaction Callbacks
When you initiate a transaction via Payarc Connect V3 and provide a callbackURL, Payarc will send the transaction result to this URL via a POST request once the terminal finishes processing. You need to set up an endpoint on your server to receive and process this callback.
For security, each callback request includes an Authorization header containing a unique Bearer token that you received when setting up your account or callback configuration. The samples below demonstrate how to create an endpoint that can receive this POST request, validate the Bearer token to ensure the callback is from our system, and then read the incoming request body, which contains the transaction result as a JSON payload.
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc;
using System.Text;
using System.Text.Json;
namespace PayArcConnectApi.Controllers {
[Route("[controller]")]
public class CallbackController : Controller {
[HttpPost]
public async Task<IActionResult> CallbackResponse() {
StringValues authHeader;
if (!Request.Headers.TryGetValue("Authorization", out authHeader)
|| StringValues.IsNullOrEmpty(authHeader)) {
return Unauthorized("Authorization header is missing.");
}
string authHeaderValue = authHeader.ToString();
if (!authHeaderValue.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) {
return Unauthorized("Authorization header must start with Bearer.");
}
string token = authHeaderValue.Substring("Bearer ".Length).Trim();
if (token != YOUR_BEARER_TOKEN) {
return Unauthorized("Invalid token.");
}
try {
using var reader = new StreamReader(HttpContext.Request.Body, Encoding.UTF8);
string requestBody = await reader.ReadToEndAsync();
var payload = JsonSerializer.Deserialize<JsonElement>(requestBody);
string traceId = payload.GetProperty("traceId").GetString();
string transType = payload.GetProperty("transType").GetString();
string status = payload.GetProperty("status").GetString();
string chargeId = payload.TryGetProperty("chargeId", out var c) ? c.GetString() : null;
string authCode = payload.TryGetProperty("authCode", out var a) ? a.GetString() : null;
var amount = payload.GetProperty("amount");
var processor = payload.GetProperty("processor");
if (status == "SUCCESS") {
Console.WriteLine($"[{transType}] Approved | traceId: {traceId} | chargeId: {chargeId} | authCode: {authCode} | approved: {amount.GetProperty("approved")} {amount.GetProperty("currency")}");
// TODO: fulfill order
} else {
Console.WriteLine($"[{transType}] Not approved | traceId: {traceId} | status: {status} | reason: {processor.GetProperty("responseText")}");
// TODO: notify operator
}
return Ok(requestBody);
} catch (IOException ex) {
Console.Error.WriteLine($"Failed to read stream from callback body: {ex.Message}");
return BadRequest("Invalid response format.");
} catch (Exception ex) {
Console.Error.WriteLine($"An error occurred processing callback: {ex.Message}");
return StatusCode(500, "An internal error occurred.");
}
}
}
}import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet("/callback")
public class CallbackServlet extends HttpServlet {
private static final ObjectMapper mapper = new ObjectMapper();
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
if (authHeader == null || authHeader.isEmpty()) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("Authorization header is missing.");
return;
}
if (!authHeader.startsWith("Bearer ")) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("Authorization header must start with Bearer.");
return;
}
String token = authHeader.substring("Bearer ".length()).trim();
if (!token.equals(YOUR_BEARER_TOKEN)) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("Invalid token.");
return;
}
response.setContentType("text/plain");
response.setCharacterEncoding("UTF-8");
StringBuilder requestBodyBuilder = new StringBuilder();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream(), "UTF-8"))) {
String line;
while ((line = reader.readLine()) != null) {
requestBodyBuilder.append(line);
}
} catch (IOException ex) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.getWriter().write("Invalid request format.");
return;
} catch (Exception ex) {
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
response.getWriter().write("An internal error occurred.");
return;
}
String requestBody = requestBodyBuilder.toString();
if (requestBody == null || requestBody.trim().isEmpty()) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.getWriter().write("Empty request body.");
return;
}
try {
JsonNode payload = mapper.readTree(requestBody);
String traceId = payload.path("traceId").asText();
String transType = payload.path("transType").asText();
String status = payload.path("status").asText();
String chargeId = payload.path("chargeId").asText(null);
String authCode = payload.path("authCode").asText(null);
JsonNode amount = payload.path("amount");
JsonNode processor = payload.path("processor");
if ("SUCCESS".equals(status)) {
System.out.printf("[%s] Approved | traceId: %s | chargeId: %s | authCode: %s | approved: %s %s%n",
transType, traceId, chargeId, authCode,
amount.path("approved").asText(), amount.path("currency").asText());
// TODO: fulfill order
} else {
System.out.printf("[%s] Not approved | traceId: %s | status: %s | reason: %s%n",
transType, traceId, status, processor.path("responseText").asText());
// TODO: notify operator
}
} catch (Exception ex) {
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
response.getWriter().write("An internal error occurred.");
return;
}
response.setStatus(HttpServletResponse.SC_OK);
response.getWriter().write(requestBody);
}
}async function callbackResponse(req, res) {
try {
const authHeader = req.headers['authorization'];
if (!authHeader) {
res.writeHead(401, { 'Content-Type': 'text/plain' });
res.end('Authorization header is missing.');
return;
}
if (!authHeader.startsWith('Bearer ')) {
res.writeHead(401, { 'Content-Type': 'text/plain' });
res.end('Authorization header must start with Bearer.');
return;
}
const token = authHeader.substring('Bearer '.length).trim();
if (token != YOUR_BEARER_TOKEN) {
res.writeHead(401, { 'Content-Type': 'text/plain' });
res.end('Invalid token.');
return;
}
let requestBody = '';
for await (const chunk of req) {
requestBody += chunk.toString();
}
const payload = JSON.parse(requestBody);
const { traceId, transType, status, chargeId, authCode, amount, processor } = payload;
if (status === 'SUCCESS') {
console.log(`[${transType}] Approved | traceId: ${traceId} | chargeId: ${chargeId} | authCode: ${authCode} | approved: ${amount.approved} ${amount.currency}`);
// TODO: fulfill order
} else {
console.warn(`[${transType}] Not approved | traceId: ${traceId} | status: ${status} | reason: ${processor?.responseText}`);
// TODO: notify operator
}
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end(requestBody);
} catch (error) {
res.writeHead(400, { 'Content-Type': 'text/plain' });
res.end('Invalid request format.');
}
}<?php
header('Content-Type: text/plain');
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
if (empty($authHeader)) {
http_response_code(401);
echo "Authorization header is missing.";
exit;
}
if (!str_starts_with($authHeader, 'Bearer ')) {
http_response_code(401);
echo "Authorization header must start with Bearer.";
exit;
}
$token = substr($authHeader, strlen('Bearer '));
if ($token != YOUR_BEARER_TOKEN) {
http_response_code(401);
echo "Invalid token.";
exit;
}
$requestBody = file_get_contents('php://input');
if ($requestBody === false) {
http_response_code(400);
echo "Invalid request format.";
exit;
}
if (trim($requestBody) === '') {
http_response_code(400);
echo "Empty request body.";
exit;
}
$payload = json_decode($requestBody, true);
$traceId = $payload['traceId'] ?? '';
$transType = $payload['transType'] ?? '';
$status = $payload['status'] ?? '';
$chargeId = $payload['chargeId'] ?? '';
$authCode = $payload['authCode'] ?? '';
$approved = $payload['amount']['approved'] ?? '';
$currency = $payload['amount']['currency'] ?? '';
$respText = $payload['processor']['responseText'] ?? '';
if ($status === 'SUCCESS') {
error_log("[{$transType}] Approved | traceId: {$traceId} | chargeId: {$chargeId} | authCode: {$authCode} | approved: {$approved} {$currency}");
// TODO: fulfill order
} else {
error_log("[{$transType}] Not approved | traceId: {$traceId} | status: {$status} | reason: {$respText}");
// TODO: notify operator
}
http_response_code(200);
echo $requestBody;
?>from flask import Flask, request, Response
import json
app = Flask(__name__)
@app.route('/callback', methods=['POST'])
def callback_response():
try:
auth_header = request.headers.get('Authorization')
if not auth_header:
return Response("Authorization header is missing.", status=401)
if not auth_header.startswith('Bearer '):
return Response("Authorization header must start with Bearer.", status=401)
token = auth_header[len('Bearer '):].strip()
if token != YOUR_BEARER_TOKEN:
return Response("Invalid token.", status=401)
request_body = request.data.decode('utf-8')
if not request_body:
return Response("Empty request body.", status=400)
payload = json.loads(request_body)
trace_id = payload.get('traceId')
trans_type = payload.get('transType')
status = payload.get('status')
charge_id = payload.get('chargeId')
auth_code = payload.get('authCode')
amount = payload.get('amount', {})
processor = payload.get('processor', {})
if status == 'SUCCESS':
print(f"[{trans_type}] Approved | traceId: {trace_id} | chargeId: {charge_id} | authCode: {auth_code} | approved: {amount.get('approved')} {amount.get('currency')}")
# TODO: fulfill order
else:
print(f"[{trans_type}] Not approved | traceId: {trace_id} | status: {status} | reason: {processor.get('responseText')}")
# TODO: notify operator
return Response(request_body, status=200, mimetype='text/plain')
except Exception as e:
return Response("An internal error occurred.", status=500)import * as http from 'http';
interface CallbackPayload {
traceId: string;
transType: 'SALE' | 'AUTH' | 'POSTAUTH' | 'REFUND' | 'VOID';
status: string;
chargeId?: string;
authCode?: string;
amount: { total: number; subtotal: number; approved: number; currency: string; tip?: number; tax?: number };
card: { brand: string; entryMode: string; last4: string };
processor: { type: string; responseCode: string; responseText: string };
timestamp: string;
transactionId: string;
metadata?: Record<string, string>;
error?: { code: string; message: string; friendlyMessage: string } | null;
}
async function callbackResponse(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
try {
const authHeader = req.headers['authorization'];
if (!authHeader) {
res.writeHead(401, { 'Content-Type': 'text/plain' });
res.end('Authorization header is missing.');
return;
}
if (!authHeader.startsWith('Bearer ')) {
res.writeHead(401, { 'Content-Type': 'text/plain' });
res.end('Authorization header must start with Bearer.');
return;
}
const token = authHeader.substring('Bearer '.length).trim();
if (token != YOUR_BEARER_TOKEN) {
res.writeHead(401, { 'Content-Type': 'text/plain' });
res.end('Invalid token.');
return;
}
let requestBody = '';
for await (const chunk of req) {
requestBody += chunk.toString();
}
const payload = JSON.parse(requestBody) as CallbackPayload;
const { traceId, transType, status, chargeId, authCode, amount, processor } = payload;
if (status === 'SUCCESS') {
console.log(`[${transType}] Approved | traceId: ${traceId} | chargeId: ${chargeId} | authCode: ${authCode} | approved: ${amount.approved} ${amount.currency}`);
// TODO: fulfill order
} else {
console.warn(`[${transType}] Not approved | traceId: ${traceId} | status: ${status} | reason: ${processor?.responseText}`);
// TODO: notify operator
}
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end(requestBody);
} catch (error: any) {
res.writeHead(400, { 'Content-Type': 'text/plain' });
res.end('Invalid request format.');
}
}Callback Response
The callback you receive will contain a JSON object with the following structure:
{
"transactionId": "string",
"transType": "string",
"status": "string",
"chargeId": "string",
"authCode": "string",
"amount": {
"total": 0,
"subtotal": 0,
"approved": 0,
"currency": "string",
"tip": 0,
"tax": 0
},
"card": {
"brand": "string",
"entryMode": "string",
"last4": "string"
},
"processor": {
"type": "string",
"responseCode": "string",
"responseText": "string"
},
"timestamp": "string",
"traceId": "string",
"metadata": {},
"error": {
"code": "string",
"message": "string",
"friendlyMessage": "string"
}
}transactionId: Transaction identifier from the original API request..transType: Transaction type of the given transaction.status: Terminal callback status text (free-form).
Examples include APPROVED, DECLINE, DUP TRANSACTION, TIMEOUT.
Always inspect this field to determine the final transaction outcome.chargeId: Processor/network charge identifier when a charge is created.authCode: Authorization code for approved transactions when available.amount: Amount values in cents (minor units).total: Requested total amount for the transaction.subtotal: Requested pre-tip and pre-tax subtotal.approved: Approved amount for this transaction result.currency: The three-letter alphabetic ISO currency code.tip: Approved amount for tip.tax: Approved amount for tax.
card: Card information returned by the processor when available.brand: Card brand.entryMode: Card capture method (Examples: CONTACTLESS, CHIP, SWIPE, MANUAL).last4: Last four digits of the card number.
processor: Processor response metadata for the transaction attempt.type: Processor used for this transaction.responseCode: Processor response code.responseText: Processor response text, indicates the processor's reason for a non-success response.
timestamp: UTC timestamp when this callback payload was generated.traceId: Correlation identifier matching the initial API response traceId.metadata: Original caller-supplied metadata from the transaction request, echoed back for end-to-end correlation.error: Error details for API/processing errors when available. The error object is ONLY included on API errors, not processor declines, timeouts, etc.code: The corresponding error code.message: The message that corresponds to the error.friendlyMessage: A friendlier, more descriptive message in regards to the error.