Developer dashboard

Webhooks#

Receive real-time notifications about events in your AuxCORE account.


Overview#

Webhooks allow AuxCORE to push real-time notifications to your server when a payment changes state:

  • ✅ Transactions approved, pending or declined
  • 💰 Refunds processed or failed
  • ↩️ ACH debits returned
  • 🚫 Transactions voided
  • ⌛ Hosted checkout sessions cancelled or expired

Instead of polling our API, webhooks push data to you as events happen.


How Webhooks Work#

  1. Event occurs (e.g., transaction approved)
  2. AuxCORE sends an HTTP POST with a JSON body to every active endpoint of that merchant subscribed to the event
  3. Your server responds with any 2xx status within 30 seconds
  4. If it fails, AuxCORE retries with exponential backoff

Setup#

1. Create a Webhook Endpoint#

Create an endpoint on your server to receive webhook notifications. Keep the raw request body — the signature is computed over the exact bytes sent.

javascript
// Express.js example
app.post('/webhooks/auxcore', express.raw({ type: 'application/json' }), async (req, res) => {
  const signature = req.headers['x-webhook-signature'];
  const rawBody = req.body.toString('utf8');

  if (!verifySignature(rawBody, signature, process.env.WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }

  try {
    await handleWebhookEvent(JSON.parse(rawBody));
    res.status(200).send('OK');
  } catch (error) {
    console.error('Webhook error:', error);
    res.status(500).send('Error processing webhook');
  }
});

2. Register Your Webhook URL#

Webhook endpoints belong to a merchant. Register them with a dashboard JWT on the Admin API, or with an API key through the Public API.

Create an endpoint (Admin API)#

POST/api/v1/webhooks/endpoints
Body field Type Required Description
url string Yes Your HTTPS endpoint (must be a valid URL)
events string[] Yes Event types to subscribe to — see Event Types. Unknown names are rejected
description string No Label shown in the dashboard
merchantId string No Admins only: the merchant to create it for. Other users always create it for their own merchant
maxRetries integer No Total delivery attempts, default 3
retryDelaySeconds integer No Base retry delay, default 60
bash
curl -X POST https://app.auxcore.net/api/v1/webhooks/endpoints \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yoursite.com/webhooks/auxcore",
    "events": [
      "transaction.approved",
      "transaction.declined",
      "transaction.refunded",
      "transaction.refund_failed"
    ],
    "description": "Production webhook"
  }'

Response (201):

json
{
  "success": true,
  "data": {
    "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
    "tenantId": "your-tenant",
    "merchantId": "550e8400-e29b-41d4-a716-446655440000",
    "isSystemWebhook": false,
    "url": "https://yoursite.com/webhooks/auxcore",
    "description": "Production webhook",
    "events": ["transaction.approved", "transaction.declined", "transaction.refunded", "transaction.refund_failed"],
    "signatureHeader": "X-Webhook-Signature",
    "isActive": true,
    "maxRetries": 3,
    "retryDelaySeconds": 60,
    "hasSecret": true,
    "secretHint": "…9f3a",
    "secret": "4f1d0c6b2a9e8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1e0d9c8b7a6f5e9f3a"
  },
  "message": "Store the signing secret now — it cannot be retrieved again."
}

⚠️ Save the secret! It is returned only here (and by regenerate-secret). List, get and update responses show only hasSecret and secretHint.

Errors: 400 VALIDATION_ERROR (url or events missing), 400 UNKNOWN_EVENT_TYPE (lists the valid events), 400 INVALID_URL.

Register with an API key (Public API)#

POST/api/v1/public/register-webhook

Used by plugins and server integrations. Requires x-api-key; the merchant comes from the key (or X-Merchant-ID for ERP keys).

Body field Type Required Description
url string Yes Your endpoint URL
events string[] No Defaults to the ten transaction and session events listed below (dispute events must be listed explicitly)
bash
curl -X POST https://app.auxcore.net/api/v1/public/register-webhook \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://yoursite.com/webhooks/auxcore" }'

The call is an upsert by URL. A new URL returns 201 with the endpoint and data.secret. A URL that is already registered returns 200, updates its events, re-activates it, resets maxRetries / retryDelaySeconds to 3 / 60 and rotates the secret — the old secret stops verifying immediately, so store the new data.secret. A missing url returns 400 with error: "url is required"; if the rotation issues no secret the call returns 502 SECRET_ROTATION_FAILED.


Webhook Payload Structure#

All webhooks follow this format:

json
{
  "id": "0b8f3d2c-5a1e-4f6b-9c7d-2e4a6b8c0d1f",
  "type": "transaction.approved",
  "created": "2026-08-10T12:34:56.000Z",
  "data": {
    // Event-specific data
  },
  "metadata": {
    "tenantId": "your-tenant",
    "merchantId": "550e8400-e29b-41d4-a716-446655440000"
  }
}

Fields#

Field Type Description
id string Unique event ID (also sent as X-Webhook-Event-ID)
type string Event type (e.g. transaction.approved)
created string ISO 8601 time the event was created
data object Event-specific data
metadata.tenantId string Your tenant ID
metadata.merchantId string Associated merchant ID

Request headers#

Header Description
X-Webhook-Signature t=<unix seconds>,v1=<hex HMAC-SHA256>
X-Webhook-Event-Type Same as type
X-Webhook-Event-ID Same as id — use it to de-duplicate
X-Webhook-Delivery-ID Unique per delivery attempt chain
User-Agent AuxVault-Webhooks/1.0
Content-Type application/json

Event Types#

These are the only events you can subscribe to:

Event Sent when
transaction.approved A sale or authorization is approved, or later settles
transaction.pending A payment is accepted but not yet settled (e.g. ACH)
transaction.in_review A payment is approved by the processor but held by one of the merchant's Transaction Rules until the merchant reviews it — keep the order on hold until transaction.approved or transaction.voided
transaction.declined A payment is declined
transaction.voided A transaction is voided
transaction.refunded A refund is approved
transaction.refund_failed A refund is declined
transaction.returned An ACH debit is returned by the bank after it was accepted (the R-code is in data.responseText, e.g. ACH returned (R01) — ...). Hosted-checkout payments are also announced as transaction.declined with the same body, for older stores
session.cancelled The shopper cancels a hosted checkout session
session.expired A hosted checkout session expires, or the shopper never completes payment
chargeback.received A dispute case is opened (API, processor notification or bank-file import)
transaction.chargeback A transaction is flagged as charged back from the dashboard
chargeback.open A case's status changes to open
chargeback.under_review A case's status changes to under_review
chargeback.won A case's status changes to won
chargeback.lost A case's status changes to lost — including when the merchant accepts the dispute
chargeback.expired A case's status changes to expired (lapsed or prevented)

The ten transaction and session events are the default set. Dispute events are sent only to endpoints that list them in events — see Dispute Events for their payload.

Transaction event data#

All transaction.* events carry the same data body:

json
{
  "id": "0b8f3d2c-5a1e-4f6b-9c7d-2e4a6b8c0d1f",
  "type": "transaction.approved",
  "created": "2026-08-10T12:34:56.000Z",
  "data": {
    "transactionId": "TXN-1776037573138-56123064",
    "id": "550e8400-e29b-41d4-a716-446655440020",
    "type": "sale",
    "status": "approved",
    "amount": 153.75,
    "baseAmount": 150.00,
    "convenienceFee": 3.75,
    "tipAmount": 0,
    "taxAmount": 0,
    "currency": "USD",
    "authCode": "123456",
    "responseCode": "100",
    "responseText": "Approved",
    "gatewayTransactionId": "8812734455",
    "gatewayName": "NMI",
    "provider": "NMI",
    "paymentMethod": "card",
    "cardLast4": "1111",
    "cardBrand": "visa",
    "orderId": "1042",
    "invoiceNumber": null,
    "billingEmail": "customer@example.com",
    "originalTransactionId": null,
    "requestOrigin": "api",
    "payment": { "method": "card" },
    "merchantId": "550e8400-e29b-41d4-a716-446655440000",
    "timestamp": "2026-08-10T12:34:56.000Z"
  },
  "metadata": {
    "tenantId": "your-tenant",
    "merchantId": "550e8400-e29b-41d4-a716-446655440000"
  }
}
  • payment.method is card, ach, crypto or cash.
  • Hosted checkout payments also carry sessionId and refNo.
  • For refunds (transaction.refunded / transaction.refund_failed), originalTransactionId points at the sale.

Session event data#

json
{
  "sessionId": "cs_3f9c2b7e0d",
  "refNo": "REF-1042",
  "orderId": "1042",
  "merchantId": "550e8400-e29b-41d4-a716-446655440000",
  "transactionId": "TXN-1776037573138-56123064",
  "status": "error",
  "reason": "checkout_not_completed",
  "timestamp": "2026-08-10T12:34:56.000Z"
}

Note: Subscription, invoice, chargeback and settlement events are not available as webhooks.


Signature Verification#

Always verify webhook signatures to ensure requests are from AuxCORE.

How It Works#

The X-Webhook-Signature header looks like t=1786451696,v1=5f2b…. AuxCORE computes v1 as the hex HMAC-SHA256, keyed with your endpoint secret, of the string <t>.<raw request body>. Reject the request if the signature does not match, or if t is more than 5 minutes from your clock (replay protection).

Node.js Example#

javascript
const crypto = require('crypto');

function verifySignature(rawBody, header, secret, toleranceSeconds = 300) {
  if (!header) return false;
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  const timestamp = Number(parts.t);
  if (!timestamp || !parts.v1) return false;
  if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`, 'utf8')
    .digest('hex');

  const a = Buffer.from(parts.v1, 'hex');
  const b = Buffer.from(expected, 'hex');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Python Example#

python
import hmac
import hashlib
import os
import time

def verify_signature(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    if not header:
        return False
    parts = dict(p.split('=', 1) for p in header.split(','))
    timestamp = int(parts.get('t', 0))
    v1 = parts.get('v1')
    if not timestamp or not v1 or abs(time.time() - timestamp) > tolerance:
        return False
    signed = f"{timestamp}.".encode() + raw_body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(v1, expected)

# In your webhook handler (Flask)
@app.route('/webhooks/auxcore', methods=['POST'])
def webhook():
    if not verify_signature(request.get_data(), request.headers.get('X-Webhook-Signature'), os.environ['WEBHOOK_SECRET']):
        return 'Invalid signature', 401
    # Process webhook
    return 'OK', 200

Handling Webhooks#

Best Practices#

  1. Respond quickly - Return a 2xx immediately (the request times out after 30 seconds)
  2. Process asynchronously - Queue heavy processing
  3. Be idempotent - Handle duplicate events gracefully
  4. Verify signatures - Always verify before processing
  5. Log events - Keep audit trail of all webhooks
  6. Handle retries - Use X-Webhook-Event-ID to deduplicate

Example Handler#

javascript
const processedEvents = new Set();

async function handleWebhookEvent(payload) {
  const { id, type, data } = payload;
  
  // Check if already processed (idempotency)
  if (processedEvents.has(id)) {
    console.log(`Event ${id} already processed`);
    return;
  }
  
  // Mark as processed
  processedEvents.add(id);
  
  // Route to appropriate handler
  switch (type) {
    case 'transaction.approved':
      await markOrderPaid(data.orderId, data.transactionId);
      break;

    case 'transaction.declined':
      await markOrderFailed(data.orderId, data.responseText);
      break;

    case 'transaction.returned':
      await reverseOrder(data.orderId, data.responseText);
      break;

    case 'transaction.refunded':
      await recordRefund(data.originalTransactionId, data.amount);
      break;

    default:
      console.log(`Unhandled event type: ${type}`);
  }
}

Retry Logic#

A delivery fails on any non-2xx response, a network error, or no response within 30 seconds. Failed deliveries are retried until the endpoint's maxRetries attempts (default 3, including the first) have been made. The delay before attempt n + 1 is retryDelaySeconds × 2^(n − 1):

Attempt Delay (defaults)
1 Immediate
2 ~1 minute
3 ~2 minutes

Retries are picked up by a sweep that runs about once a minute, so actual delays can be up to a minute longer. After the last attempt the delivery is marked failed; you can retry it manually (see Managing Webhooks).


Testing Webhooks#

Local Development with ngrok#

bash
# Install ngrok
npm install -g ngrok

# Start your local server
node server.js  # Running on localhost:3000

# Create tunnel
ngrok http 3000

# Use the ngrok URL as your webhook URL
# https://abc123.ngrok.io/webhooks/auxcore

Send a Test Event#

POST/api/v1/webhooks/endpoints/:id/test

Sends a signed webhook.test event (data: { message, timestamp }) to the endpoint and returns the delivery result.

bash
curl -X POST https://app.auxcore.net/api/v1/webhooks/endpoints/7c9e6679-7425-40de-944b-e07fc1f90ae7/test \
  -H "Authorization: Bearer YOUR_TOKEN"

Managing Webhooks#

All management routes require Authorization: Bearer <JWT>. Admins see every endpoint in the tenant (filter with ?merchantId=); other users see only their merchant's.

List Webhooks#

GET/api/v1/webhooks/endpoints
bash
curl https://app.auxcore.net/api/v1/webhooks/endpoints \
  -H "Authorization: Bearer YOUR_TOKEN"

Get a Webhook#

GET/api/v1/webhooks/endpoints/:id

Update Webhook#

PATCH/api/v1/webhooks/endpoints/:id

Any of url, description, events, isActive, maxRetries, retryDelaySeconds.

bash
curl -X PATCH https://app.auxcore.net/api/v1/webhooks/endpoints/7c9e6679-7425-40de-944b-e07fc1f90ae7 \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "events": ["transaction.approved", "transaction.declined"],
    "isActive": true
  }'

Delete Webhook#

DELETE/api/v1/webhooks/endpoints/:id
bash
curl -X DELETE https://app.auxcore.net/api/v1/webhooks/endpoints/7c9e6679-7425-40de-944b-e07fc1f90ae7 \
  -H "Authorization: Bearer YOUR_TOKEN"

Rotate the Signing Secret#

POST/api/v1/webhooks/endpoints/:id/regenerate-secret

Returns the new secret in data.secret. The old secret stops verifying immediately.

Delivery History#

GET/api/v1/webhooks/endpoints/:id/deliveries

Retry a Delivery#

POST/api/v1/webhooks/deliveries/:deliveryId/retry

Security Best Practices#

✅ DO:#

  • Verify signatures on every request, including the timestamp
  • Use HTTPS for webhook URLs (the API only checks that the URL is well-formed)
  • Log all webhooks for audit trail
  • Use idempotency to handle duplicates
  • Respond quickly (< 5 seconds; hard limit 30 seconds)
  • Queue heavy processing for async handling

❌ DON'T:#

  • Don't skip signature verification
  • Don't re-serialize the body before verifying — use the raw bytes
  • Don't process synchronously if it takes > 5 seconds
  • Don't trust the data without verification

Troubleshooting#

Webhook Not Received#

  1. Check the endpoint is active and subscribed to the event (GET /api/v1/webhooks/endpoints)
  2. Verify the URL is correct and publicly reachable
  3. Check firewall/IP whitelist settings
  4. Review delivery history (GET /api/v1/webhooks/endpoints/:id/deliveries)

Signature Verification Fails#

  1. Ensure you're using the current secret — registering the same URL again through register-webhook, or regenerate-secret, rotates it
  2. Verify you're signing <t>.<raw body>, not a re-serialized object
  3. Check your server clock — signatures older than 5 minutes should be rejected
  4. Make sure charset is UTF-8

Webhooks Timing Out#

  1. Respond with 200 OK immediately
  2. Queue processing for async handling
  3. Don't wait for external services
  4. Keep response time < 5 seconds

Complete Event Reference#

View all webhook events →


Next Steps#


Need help? Contact support@auxcore.net