Developer dashboard

Refunds#

Process full and partial refunds for card and ACH transactions.


Overview#

The Refunds API allows you to return funds to customers for completed transactions. You can:

  • 💰 Full refunds - Return the entire transaction amount
  • 📊 Partial refunds - Return a portion of the transaction amount
  • 🔄 Multiple refunds - Issue multiple partial refunds up to the original amount
  • ⚡ Instant processing - Refunds are sent to the gateway immediately
  • 🔁 Decline queue - Refunds declined by the gateway are queued in the Refund Decline Queue for retry

Each refund is recorded as a new transaction of type: "refund" with a REF-… ID and originalTransactionId pointing at the sale. The original sale's status becomes refunded once its full amount has been refunded; after a partial refund it keeps its status (approved or settled).


API Route Options#

Auth Type Endpoint Use Case
API Key (x-api-key) POST /api/v1/public/refund ERP / server integration
JWT Bearer POST /api/v1/transactions/:id/refund Dashboard / admin (requires the transactions.refund permission)
JWT Bearer POST /api/v1/transactions/:id/refund/partial Partial refund (amount required)
SDK / API key POST /api/v1/auxvault/refund Official v2.0 SDK namespace; server-side x-api-key, SDK public keys get 403

For ERP and server-to-server integrations, see Refund a Transaction — Public API in the Transactions API.


Process a Refund (Admin API)#

Endpoint#

POST/api/v1/transactions/:transactionId/refund

:transactionId is the transaction UUID or its TXN-… / ACH-… / CASH-… ID.

Full Refund#

Refund the entire remaining amount:

Full Refundbash
curl -X POST https://uat.auxcore.net/api/v1/transactions/7c9e6679-7425-40de-944b-e07fc1f90ae7/refund \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "X-Tenant-ID: your-tenant-id" \
  -H "Content-Type: application/json" \
  -d '{}'

Response (201):

Full Refundjson
{
  "success": true,
  "data": {
    "id": "c2a8e1f4-3b7d-4e9a-8f12-6d5c4b3a2e10",
    "transactionId": "REF-1769614200000-9F8E7D6C",
    "type": "refund",
    "status": "refunded",
    "amount": "150.75",
    "currency": "USD",
    "originalTransactionId": "TXN-1769605200000-A1B2C3D4",
    "responseCode": "100",
    "responseText": "Approved",
    "createdAt": "2026-01-28T15:30:00.000Z"
  }
}

Partial Refund#

Send an amount to refund part of the transaction. Either endpoint works:

POST/api/v1/transactions/:transactionId/refund/partial
Partial Refundbash
curl -X POST https://uat.auxcore.net/api/v1/transactions/7c9e6679-7425-40de-944b-e07fc1f90ae7/refund/partial \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "X-Tenant-ID: your-tenant-id" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 50.00
  }'

The response has the same shape as a full refund, with amount set to the refunded value.

Request Parameters#

Field Type Description Required
amount decimal Refund amount (omit on /refund for a full refund; required and > 0 on /refund/partial) No

A reason or metadata sent in the body is accepted but not stored.

Check the refund status#

The HTTP status is 201 and success is true whenever a refund row was created — including when the gateway declined it. Always read data.status:

data.status Meaning
refunded Gateway accepted the refund
declined Gateway refused it, or no gateway credentials were available — data.responseText says why. Only a gateway decline is added to the Refund Decline Queue

TSYS Sierra exception: a full refund of a sale that has not settled yet is processed as a void, so data is the original sale with status: "voided" rather than a new REF-… row.

On POST /api/v1/public/refund only a lowercase amount makes the refund partial — a PascalCase Amount is ignored there and the full amount is refunded.


Multiple Partial Refunds#

You can issue multiple partial refunds until you reach the original transaction amount:

bash
# First partial refund
curl -X POST https://uat.auxcore.net/api/v1/public/refund \
  -H "x-api-key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"transactionId": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "amount": 30.00}'

# Second partial refund
curl -X POST https://uat.auxcore.net/api/v1/public/refund \
  -H "x-api-key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"transactionId": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "amount": 20.00}'

The system locks the original transaction, totals earlier refunded and pending refunds, and rejects any refund that would exceed the remaining amount.

Gateway-specific rules#

Case Behavior
Cash Full refund only, recorded immediately as refunded (no gateway)
TSYS Sierra, not yet settled A full refund is processed as a void; a partial refund returns 400 until the sale settles
Payliance ACH Full refund only. A debit already sent to the bank (processing) can be refunded
Pacaso Refund is requested from Pacaso by email and recorded as refunded with responseCode: "REQUESTED"

Look Up Refunds#

There is no separate refunds resource — refunds are transactions.

Get a Refund#

GET/api/v1/transactions/:id
Get a Refundbash
curl https://uat.auxcore.net/api/v1/transactions/REF-1769614200000-9F8E7D6C \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "X-Tenant-ID: your-tenant-id"

With an API key, use GET /api/v1/public/transaction/:id.

List Refunds#

GET/api/v1/transactions

Send type=refund to return refunds only, e.g. GET /api/v1/transactions?type=refund&page=1.

Query Parameters#

Parameter Type Description
type string refund
page integer Page number (default: 1)
limit integer Items per page (default: 50)
dateFrom date Filter by start date
dateTo date Filter by end date
status string Filter by status (refunded, declined, pending)
bash
curl "https://uat.auxcore.net/api/v1/transactions?type=refund&dateFrom=2026-01-01&status=refunded" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "X-Tenant-ID: your-tenant-id"

With an API key, use GET /api/v1/public/transaction-list?type=refund. The refunds for a given sale are the rows whose originalTransactionId equals the sale's transactionId; GET /api/v1/transactions/:id on the sale also returns its refundedAmount.


Refund Statuses#

Status Description
refunded Refund processed successfully
declined Refund declined by the processor, or no gateway credentials were available
pending Refund in progress (only while the request is being processed)

Declined Refunds (PRA)#

When the gateway declines a refund, it is added to the Refund Decline Queue. Declines flagged by the gateway as Protective Refund Amount (PRA) declines are also handed to the PRA handling flow.

Declined Response (201):

json
{
  "success": true,
  "data": {
    "transactionId": "REF-1769614200000-9F8E7D6C",
    "type": "refund",
    "status": "declined",
    "responseCode": "200",
    "responseText": "Transaction was declined by processor",
    "originalTransactionId": "TXN-1769605200000-A1B2C3D4"
  }
}

Each queue entry allows up to 3 retries, with exponential backoff between attempts. Retries run when the queue is processed (POST /api/v1/refund-decline-queue/process-retries, or POST /api/v1/refund-decline-queue/:id/retry for one entry) — no scheduler in AuxCORE triggers them on its own. A refund declined because no gateway credentials were available is not queued.

Learn more about Refund Decline Queue →


Refund via AuxCORE SDK#

POST/api/v1/auxvault/refund
Refund via AuxCORE SDKbash
curl -X POST https://uat.auxcore.net/api/v1/auxvault/refund \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"TransactionId": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "Amount": 50.00}'

Omit Amount for a full refund. Returns { "success": true, "Status": 1, "TransactionId": "…", "Amount": 50, "message": "Refund processed" }. success reflects only that the request was accepted — a gateway decline still returns Status: 1, so confirm the outcome by querying the transaction.


Refund Timeline#

Event Timeframe
Refund initiated Immediate
Customer sees refund 3-5 business days (card) / 3-5 business days (ACH)
Settlement Next settlement batch

⏰ Note: While refunds are processed immediately by AuxCORE, it takes 3-5 business days for funds to appear in the customer's account.


Webhooks#

Refund events carry the refund transaction in data (type: "refund", originalTransactionId set):

transaction.refunded#

json
{
  "id": "0b6f3c2e-8a41-4d7e-9c55-2f1e7a9d3b64",
  "type": "transaction.refunded",
  "created": "2026-01-28T15:30:00.000Z",
  "data": {
    "transactionId": "REF-1769614200000-9F8E7D6C",
    "type": "refund",
    "status": "refunded",
    "amount": "150.75",
    "originalTransactionId": "TXN-1769605200000-A1B2C3D4",
    "merchantId": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  },
  "metadata": { "tenantId": "your-tenant-id", "merchantId": "3fa85f64-5717-4562-b3fc-2c963f66afa6" }
}

transaction.refund_failed#

json
{
  "id": "5d2a9e71-3c84-4f0b-a6e2-9b7c1d4e8f20",
  "type": "transaction.refund_failed",
  "created": "2026-01-28T15:30:00.000Z",
  "data": {
    "transactionId": "REF-1769614200000-9F8E7D6C",
    "type": "refund",
    "status": "declined",
    "amount": "150.75",
    "responseText": "Transaction was declined by processor",
    "originalTransactionId": "TXN-1769605200000-A1B2C3D4"
  },
  "metadata": { "tenantId": "your-tenant-id", "merchantId": "3fa85f64-5717-4562-b3fc-2c963f66afa6" }
}

Best Practices#

✅ DO:#

  • Verify transaction ID before refunding
  • Check refundable amount before partial refunds (refundedAmount on the sale)
  • Check data.status on every refund response
  • Handle declined refunds via the transaction.refund_failed webhook
  • Notify customers when refund is processed
  • Track refund IDs (REF-…) for reconciliation

❌ DON'T:#

  • Don't refund more than original amount
  • Don't refund voided transactions (they were never charged)
  • Don't assume instant customer receipt
  • Don't re-send a declined refund as a new refund — retry it through the Refund Decline Queue instead

Error Handling#

Refund errors are returned as HTTP 400 with error.code: "VALIDATION_ERROR", or 404 with NOT_FOUND.

Transaction Not Refundable#

json
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Only approved or settled transactions can be refunded"
  }
}

Refund Amount Exceeds Available#

json
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Refund amount ($50.00) exceeds remaining refundable amount ($20.00). Original: $100.00, previously refunded: $80.00."
  }
}

Transaction Not Found#

json
{
  "success": false,
  "error": {
    "code": "NOT_FOUND",
    "message": "Transaction with id TXN-0000000000000-INVALID0 not found"
  }
}

Other messages you may see: Valid refund amount is required (partial refund without a positive amount), Cash refunds support full refund only, Partial refunds on TSYS are only supported after settlement. …, and This transaction has already been lost to a chargeback and cannot be refunded. ….


Code Examples#

JavaScript#

javascript
async function processRefund(transactionId, amount = null) {
  const response = await fetch('https://uat.auxcore.net/api/v1/public/refund', {
    method: 'POST',
    headers: {
      'x-api-key': apiKey,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      transactionId,
      ...(amount && { amount })
    })
  });

  const result = await response.json();

  if (result.success && result.data.status === 'refunded') {
    console.log('Refund processed:', result.data.transactionId);
  } else if (result.success) {
    console.warn('Refund declined:', result.data.responseText);
  } else {
    console.error('Refund failed:', result.error);
  }

  return result;
}

Python#

python
def process_refund(transaction_id, amount=None):
    payload = {'transactionId': transaction_id}
    if amount:
        payload['amount'] = amount

    response = requests.post(
        'https://uat.auxcore.net/api/v1/public/refund',
        headers={
            'x-api-key': api_key,
            'Content-Type': 'application/json'
        },
        json=payload
    )

    result = response.json()

    if result.get('success') and result['data']['status'] == 'refunded':
        print(f"Refund processed: {result['data']['transactionId']}")
    elif result.get('success'):
        print(f"Refund declined: {result['data'].get('responseText')}")
    else:
        print(f"Refund failed: {result['error']}")

    return result

Next Steps#


Need help? Contact support@auxcore.net