Webhooks
Webhooks allow you to receive real-time event notifications from AXRO (such as order updates, shipment notifications, etc.) by subscribing an HTTPS endpoint. When events occur, AXRO will send an HTTP POST request to your configured endpoint with a signed JSON payload.
Overview
Authentication: All webhook subscription management endpoints require Authorization: Bearer YOUR_ACCESS_TOKEN.
Security: Each subscription includes an API token that:
- Is used as the HMAC secret to sign the request body
- Should be validated on your endpoint to verify authenticity
Delivery Semantics:
- Single attempt: Webhooks are delivered once with a 10-second HTTP timeout
- Success: Any 2xx HTTP response is considered successful
- Failures: Non-2xx responses or timeouts are logged but not retried
- No Dead Letter Queue: Failed deliveries are not stored or replayed
Signature Verification: Each webhook includes:
X-Signature-Algorithm: AlwaysHMAC-SHA256X-Signature-Timestamp: Unix timestamp (seconds) when the signature was generatedX-Signature: Format issha256={hex}where hex is the HMAC-SHA256 signature of"{timestamp}.{raw_body_bytes}"using your API token as the secret
Create or Update Webhook Subscription
Create or update the webhook subscription for your customer account. If a subscription already exists, it will be updated with the new endpoint URL and token.
JSON body parameters
| Name | Type | Required | Description |
|---|---|---|---|
endpoint_url | string | ✅ | Your webhook endpoint URL. Must be https:// or http://localhost (for testing) |
api_token | string | ✅ | Token used for authentication and HMAC signature verification |
URL Validation:
https://is allowed for any domainhttp://is only allowed forlocalhost,127.0.0.1, or0.0.0.0- Invalid URLs return HTTP 422 Unprocessable Entity
Example Request
- cURL
- PHP
- Python
- Go
- C#
curl -X POST "https://api.axro.com/api/v1/webhook/subscribe/" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"endpoint_url": "https://webhooks.example.com/axro",
"api_token": "your_secure_webhook_token_here"
}'
// Using cURL
function subscribeWebhook() {
$url = 'https://api.axro.com/api/v1/webhook/subscribe/';
$token = 'YOUR_ACCESS_TOKEN';
$data = [
'endpoint_url' => 'https://webhooks.example.com/axro',
'api_token' => 'your_secure_webhook_token_here'
];
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $token,
'Content-Type: application/json'
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if (curl_errno($ch)) {
$error = curl_error($ch);
curl_close($ch);
return ['error' => $error];
}
curl_close($ch);
$responseData = json_decode($response, true);
if ($httpCode >= 200 && $httpCode < 300) {
return $responseData;
} else {
return ['error' => 'HTTP Error: ' . $httpCode, 'response' => $responseData];
}
}
// Call the function
$result = subscribeWebhook();
print_r($result);
# Using requests library
import requests
def subscribe_webhook():
url = 'https://api.axro.com/api/v1/webhook/subscribe/'
token = 'YOUR_ACCESS_TOKEN'
headers = {
'Authorization': f'Bearer {token}',
'Content-Type': 'application/json'
}
data = {
'endpoint_url': 'https://webhooks.example.com/axro',
'api_token': 'your_secure_webhook_token_here'
}
try:
response = requests.post(url, headers=headers, json=data)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print(f'Error: {e}')
return None
# Call the function
result = subscribe_webhook()
if result:
print(result)
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
)
type SubscribeRequest struct {
EndpointURL string `json:"endpoint_url"`
APIToken string `json:"api_token"`
}
func subscribeWebhook() (map[string]interface{}, error) {
url := "https://api.axro.com/api/v1/webhook/subscribe/"
token := "YOUR_ACCESS_TOKEN"
reqData := SubscribeRequest{
EndpointURL: "https://webhooks.example.com/axro",
APIToken: "your_secure_webhook_token_here",
}
jsonData, err := json.Marshal(reqData)
if err != nil {
return nil, fmt.Errorf("error marshalling JSON: %v", err)
}
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
if err != nil {
return nil, fmt.Errorf("error creating request: %v", err)
}
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("error sending request: %v", err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("error reading response: %v", err)
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("HTTP error %d: %s", resp.StatusCode, string(body))
}
var result map[string]interface{}
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("error parsing response: %v", err)
}
return result, nil
}
func main() {
result, err := subscribeWebhook()
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Println("Subscription successful!")
resultJSON, _ := json.MarshalIndent(result, "", " ")
fmt.Println(string(resultJSON))
}
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
class Program
{
class SubscribeRequest
{
[JsonPropertyName("endpoint_url")]
public string EndpointUrl { get; set; }
[JsonPropertyName("api_token")]
public string ApiToken { get; set; }
}
static async Task Main()
{
try
{
var result = await SubscribeWebhook();
Console.WriteLine("Subscription successful!");
Console.WriteLine(JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }));
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
}
static async Task SubscribeWebhook()
{
string url = "https://api.axro.com/api/v1/webhook/subscribe/";
string token = "YOUR_ACCESS_TOKEN";
var data = new SubscribeRequest
{
EndpointUrl = "https://webhooks.example.com/axro",
ApiToken = "your_secure_webhook_token_here"
};
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
string jsonData = JsonSerializer.Serialize(data);
var content = new StringContent(jsonData, Encoding.UTF8, "application/json");
HttpResponseMessage response = await client.PostAsync(url, content);
string responseContent = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
return JsonSerializer.Deserialize(responseContent);
}
else
{
throw new Exception($"HTTP error {(int)response.StatusCode}: {responseContent}");
}
}
}
}
Example Response
{
"customer_number": "123456",
"endpoint_url": "https://webhooks.example.com/axro",
"active": true,
"token_tail": "_here"
}
Response Fields:
customer_number: Your customer numberendpoint_url: The subscribed webhook URLactive: Whether the subscription is activetoken_tail: Last 4 characters of the API token (for verification)
Get Webhook Subscription
Retrieve the current webhook subscription for your customer account.
Example Request
- cURL
- PHP
- Python
- Go
- C#
curl -X GET "https://api.axro.com/api/v1/webhook/subscription/" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
// Using cURL
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://api.axro.com/api/v1/webhook/subscription/');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
'Authorization: Bearer YOUR_ACCESS_TOKEN'
));
$response = curl_exec($ch);
$error = curl_error($ch);
curl_close($ch);
if ($error) {
echo "Error: " . $error;
} else {
$data = json_decode($response, true);
print_r($data);
}
# Using requests library
import requests
url = 'https://api.axro.com/api/v1/webhook/subscription/'
headers = {'Authorization': 'Bearer YOUR_ACCESS_TOKEN'}
response = requests.get(url, headers=headers)
print(response.status_code)
print(response.json())
package main
import (
"fmt"
"io/ioutil"
"net/http"
)
func main() {
url := "https://api.axro.com/api/v1/webhook/subscription/"
req, err := http.NewRequest("GET", url, nil)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
req.Header.Add("Authorization", "Bearer YOUR_ACCESS_TOKEN")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
fmt.Println("Error sending request:", err)
return
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println("Error reading response:", err)
return
}
fmt.Println("Response Status:", resp.Status)
fmt.Println("Response Body:", string(body))
}
using System;
using System.Net.Http;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
using (HttpClient client = new HttpClient())
{
client.DefaultRequestHeaders.Add("Authorization", "Bearer YOUR_ACCESS_TOKEN");
HttpResponseMessage response = await client.GetAsync("https://api.axro.com/api/v1/webhook/subscription/");
string responseBody = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
Console.WriteLine("Response: " + responseBody);
}
else
{
Console.WriteLine("Error: " + response.StatusCode);
Console.WriteLine("Response: " + responseBody);
}
}
}
}
Example Response
{
"customer_number": "123456",
"endpoint_url": "https://webhooks.example.com/axro",
"active": true,
"token_tail": null
}
Note: The token_tail field is null when retrieving a subscription (only shown when creating/updating). Returns HTTP 404 if no subscription exists.
Delete Webhook Subscription
Delete the webhook subscription for your customer account.
Example Request
- cURL
- PHP
- Python
- Go
- C#
curl -X DELETE "https://api.axro.com/api/v1/webhook/subscription/" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
// Using cURL
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://api.axro.com/api/v1/webhook/subscription/');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
'Authorization: Bearer YOUR_ACCESS_TOKEN'
));
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($error) {
echo "Error: " . $error;
} elseif ($httpCode == 204) {
echo "Subscription deleted successfully";
} else {
echo "HTTP Status: " . $httpCode;
}
# Using requests library
import requests
url = 'https://api.axro.com/api/v1/webhook/subscription/'
headers = {'Authorization': 'Bearer YOUR_ACCESS_TOKEN'}
response = requests.delete(url, headers=headers)
if response.status_code == 204:
print("Subscription deleted successfully")
else:
print(f"HTTP Status: {response.status_code}")
package main
import (
"fmt"
"net/http"
)
func main() {
url := "https://api.axro.com/api/v1/webhook/subscription/"
req, err := http.NewRequest("DELETE", url, nil)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
req.Header.Add("Authorization", "Bearer YOUR_ACCESS_TOKEN")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
fmt.Println("Error sending request:", err)
return
}
defer resp.Body.Close()
if resp.StatusCode == 204 {
fmt.Println("Subscription deleted successfully")
} else {
fmt.Printf("HTTP Status: %d\n", resp.StatusCode)
}
}
using System;
using System.Net.Http;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
using (HttpClient client = new HttpClient())
{
client.DefaultRequestHeaders.Add("Authorization", "Bearer YOUR_ACCESS_TOKEN");
HttpResponseMessage response = await client.DeleteAsync("https://api.axro.com/api/v1/webhook/subscription/");
if (response.StatusCode == System.Net.HttpStatusCode.NoContent)
{
Console.WriteLine("Subscription deleted successfully");
}
else
{
Console.WriteLine($"HTTP Status: {response.StatusCode}");
}
}
}
}
Response
HTTP 204 No Content - The subscription was successfully deleted (no response body).
Webhook Delivery Format
When events occur, AXRO sends an HTTP POST request to your configured endpoint.
HTTP Request Details
- Method: POST
- Content-Type: application/json
- Timeout: 10 seconds
- Retries: None (single attempt only)
- Success: Any 2xx HTTP status code
HTTP Headers
Each webhook request includes the following headers:
| Header | Description |
|---|---|
Content-Type | Always application/json |
X-Signature-Algorithm | Always HMAC-SHA256 |
X-Signature-Timestamp | Unix timestamp (seconds) when the signature was generated |
X-Signature | HMAC signature in format sha256={hex} |
X-Event-ID | Unique event identifier (UUID) - use for idempotency |
X-Event-Type | Event type (e.g., customer:address:completed, order:status:changed) |
X-Customer-Number | Your customer number |
Example headers:
Content-Type: application/json
X-Signature-Algorithm: HMAC-SHA256
X-Signature-Timestamp: 1730798400
X-Signature: sha256=3f6b2c1a4e8d9f0e7c5b3a2d1f9e8c7b6a5d4e3f2c1b0a9f8e7d6c5b4a3d2e1f0
X-Event-ID: 8c1b7c3e-9c5a-4b6a-8a8e-22a6be2a8f41
X-Event-Type: order:created
X-Customer-Number: 123456
Request Body (Event Envelope)
All webhook payloads follow this canonical envelope structure:
Customer Address Verified Event Example
{
"id": "8c1b7c3e-9c5a-4b6a-8a8e-22a6be2a8f41",
"type": "customer:address:completed",
"timestamp": "2025-06-25T14:30:00Z",
"customer_number": "123456",
"data": {
"address_id": "03e6ad6b-205d-56fa-9a99-3c6acd0f9123",
"customer_number": "123456"
}
}
- Use address_id to look up the full address details via Get Customer Address API
- Use address_id to send your order via Create Order API
Order Status Changed Event Example
{
"id": "8c1b7c3e-9c5a-4b6a-8a8e-22a6be2a8f41",
"type": "order:status:changed",
"timestamp": "2025-06-25T14:30:00Z",
"customer_number": "123456",
"data": {
"customer_number": "123456",
"order_number": "233945",
"shipment_number": "9876543",
"commission": "my custom reference"
}
}
- Use order_number to look up the full order details via Get Order API
- Use shipment_number to look up shipment details via Get Shipment API
- Use shipment_number to look up tracking info via Get Tracking API
Order Tracking Created Event Example
{
"id": "8c1b7c3e-9c5a-4b6a-8a8e-22a6be2a8f41",
"type": "order:tracking:created",
"timestamp": "2025-06-25T14:30:00Z",
"customer_number": "123456",
"data": {
"customer_number": "123456",
"shipment_number": "9876543",
"tracking_code": "1Z657290bb5140c1234",
"carrier": "UPS",
"commission": "my custom reference"
}
}
- Use shipment_number to look up shipment details via Get Shipment API
- Use shipment_number to look up tracking info via Get Tracking API
- Use tracking_code and carrier to provide tracking updates to your customers
Envelope Fields:
id: Unique event ID - use for deduplicationtype: Event type (matchesX-Event-Typeheader)timestamp: ISO 8601 UTC timestamp with Z suffixcustomer_number: Your customer numberdata: Event-specific payload (structure varies by event type)
Signature Verification
To verify the webhook signature and ensure the request is authentic:
-
Extract the signature components:
- Get the
X-Signature-Timestampheader value - Get the
X-Signatureheader value and remove thesha256=prefix - Read the raw request body bytes (do not parse or modify)
- Get the
-
Construct the signed string:
string_to_sign = "{timestamp}.{raw_body_bytes}"Example:
"1730798400.{\"id\":\"8c1b..."}"(timestamp as string, followed by dot, followed by the exact raw body) -
Compute the expected signature:
expected_signature = hex(hmac_sha256(api_token, string_to_sign)) -
Compare signatures:
- Use constant-time comparison to prevent timing attacks
- The computed signature should match the value from the
X-Signatureheader (after removingsha256=)
-
Validate timestamp (recommended):
- Check that the timestamp is recent (e.g., within 5 minutes)
- This prevents replay attacks
Example verification code:
- PHP
- Python
- Go
- C#
function verifyWebhookSignature($apiToken, $rawBody, $timestamp, $signature) {
// Remove 'sha256=' prefix from signature
$receivedSig = str_replace('sha256=', '', $signature);
// Construct the string to sign
$stringToSign = $timestamp . '.' . $rawBody;
// Compute expected signature
$expectedSig = hash_hmac('sha256', $stringToSign, $apiToken);
// Constant-time comparison
if (!hash_equals($expectedSig, $receivedSig)) {
return false;
}
// Verify timestamp is recent (within 5 minutes)
$now = time();
if (abs($now - intval($timestamp)) > 300) {
return false;
}
return true;
}
// Usage in your webhook endpoint
$apiToken = 'your_secure_webhook_token_here';
$rawBody = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_SIGNATURE_TIMESTAMP'];
$signature = $_SERVER['HTTP_X_SIGNATURE'];
if (!verifyWebhookSignature($apiToken, $rawBody, $timestamp, $signature)) {
http_response_code(401);
exit('Invalid signature');
}
// Process the webhook...
$payload = json_decode($rawBody, true);
import hmac
import hashlib
import time
def verify_webhook_signature(api_token: str, raw_body: bytes, timestamp: str, signature: str) -> bool:
# Remove 'sha256=' prefix from signature
received_sig = signature.replace('sha256=', '')
# Construct the string to sign
string_to_sign = f"{timestamp}.".encode('utf-8') + raw_body
# Compute expected signature
expected_sig = hmac.new(
api_token.encode('utf-8'),
string_to_sign,
hashlib.sha256
).hexdigest()
# Constant-time comparison
if not hmac.compare_digest(expected_sig, received_sig):
return False
# Verify timestamp is recent (within 5 minutes)
now = int(time.time())
if abs(now - int(timestamp)) > 300:
return False
return True
# Usage in your Flask/FastAPI webhook endpoint
# raw_body = await request.body() # FastAPI
# raw_body = request.get_data() # Flask
# api_token = 'your_secure_webhook_token_here'
# timestamp = request.headers.get('X-Signature-Timestamp')
# signature = request.headers.get('X-Signature')
#
# if not verify_webhook_signature(api_token, raw_body, timestamp, signature):
# return JSONResponse(status_code=401, content={"error": "Invalid signature"})
package main
import (
"crypto/hmac"
"crypto/sha256"
"crypto/subtle"
"encoding/hex"
"fmt"
"io/ioutil"
"net/http"
"strconv"
"strings"
"time"
)
func verifyWebhookSignature(apiToken string, rawBody []byte, timestamp string, signature string) bool {
// Remove 'sha256=' prefix from signature
receivedSig := strings.TrimPrefix(signature, "sha256=")
// Construct the string to sign
stringToSign := timestamp + "."
stringToSignBytes := append([]byte(stringToSign), rawBody...)
// Compute expected signature
h := hmac.New(sha256.New, []byte(apiToken))
h.Write(stringToSignBytes)
expectedSig := hex.EncodeToString(h.Sum(nil))
// Constant-time comparison
if subtle.ConstantTimeCompare([]byte(expectedSig), []byte(receivedSig)) != 1 {
return false
}
// Verify timestamp is recent (within 5 minutes)
ts, err := strconv.ParseInt(timestamp, 10, 64)
if err != nil {
return false
}
now := time.Now().Unix()
if abs(now-ts) > 300 {
return false
}
return true
}
func abs(n int64) int64 {
if n < 0 {
return -n
}
return n
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
apiToken := "your_secure_webhook_token_here"
// Read raw body
rawBody, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, "Cannot read body", http.StatusBadRequest)
return
}
defer r.Body.Close()
// Extract headers
timestamp := r.Header.Get("X-Signature-Timestamp")
signature := r.Header.Get("X-Signature")
// Verify signature
if !verifyWebhookSignature(apiToken, rawBody, timestamp, signature) {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
// Process webhook...
fmt.Fprintf(w, "Webhook received")
}
using System;
using System.Security.Cryptography;
using System.Text;
public class WebhookVerifier
{
public static bool VerifyWebhookSignature(string apiToken, byte[] rawBody, string timestamp, string signature)
{
// Remove 'sha256=' prefix from signature
string receivedSig = signature.Replace("sha256=", "");
// Construct the string to sign
byte[] timestampBytes = Encoding.UTF8.GetBytes(timestamp + ".");
byte[] stringToSignBytes = new byte[timestampBytes.Length + rawBody.Length];
Buffer.BlockCopy(timestampBytes, 0, stringToSignBytes, 0, timestampBytes.Length);
Buffer.BlockCopy(rawBody, 0, stringToSignBytes, timestampBytes.Length, rawBody.Length);
// Compute expected signature
using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(apiToken)))
{
byte[] hash = hmac.ComputeHash(stringToSignBytes);
string expectedSig = BitConverter.ToString(hash).Replace("-", "").ToLower();
// Constant-time comparison
if (!ConstantTimeEquals(expectedSig, receivedSig))
{
return false;
}
}
// Verify timestamp is recent (within 5 minutes)
long ts = long.Parse(timestamp);
long now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
if (Math.Abs(now - ts) > 300)
{
return false;
}
return true;
}
private static bool ConstantTimeEquals(string a, string b)
{
if (a.Length != b.Length)
return false;
int result = 0;
for (int i = 0; i < a.Length; i++)
{
result |= a[i] ^ b[i];
}
return result == 0;
}
}
Best Practices
- Respond quickly: Return a 2xx status code as soon as you receive and verify the webhook. Process the event asynchronously if needed.
- Idempotency: Use the
X-Event-IDheader to track processed events and prevent duplicate processing. - Validate signatures: Always verify the HMAC signature before processing the payload.
- Check timestamps: Reject webhooks with timestamps older than 5 minutes to prevent replay attacks.
- Log deliveries: Keep logs of received webhooks for debugging and auditing.
- Handle failures gracefully: Since there are no retries, ensure your endpoint is highly available.
- Use HTTPS: Always use HTTPS endpoints in production to protect the webhook data in transit.