Refunds v1 Policy API Documentation
Overview
The Refunds v1 policy provides comprehensive protection for refund endpoints with payment capabilities, assurance levels, and transaction limits. This policy enforces strict validation rules to prevent fraud and ensure financial security.
Policy Features
ā Core Features
- Multi-Currency Support: Direct currency-to-currency validation (no conversion)
- Atomic Daily Caps: Race-safe daily spending limits per currency
- Idempotency Protection: Prevents duplicate refunds with 24-hour deduplication
- Assurance Tiers: Amount-based assurance level requirements
- Cross-Currency Denial: Explicitly denies cross-currency refunds in v1
- Order Balance Validation: Prevents over-refunding orders
- Region Validation: Geographic restrictions
- Reason Code Validation: Enforced refund reason codes
š Security Features
- Amount Precision Validation: Currency-specific decimal place enforcement
- Bounds Checking: Prevents extreme amounts and negative values
- Rate Limiting: Per-IP rate limiting to prevent DoS attacks
- Verifiable Attestation: Comprehensive Verifiable Attestation for all decisions
- Cryptographic Signing: Decision receipts with HMAC verification
API Endpoints
Policy Verification
Endpoint: POST /api/verify/policy/finance.payment.refund.v1
Description: Verify refund request against finance.payment.refund.v1 policy
Headers:
Content-Type: application/json
X-Agent-Passport-Id: agents/your_agent_id
Request Body:
{
"agent_id": "agents/your_agent_id",
"context": {
"order_id": "ORD-12345",
"customer_id": "CUST-67890",
"amount_minor": 5000,
"currency": "USD",
"region": "US",
"reason_code": "customer_request",
"idempotency_key": "unique_key_123",
"order_currency": "USD",
"order_total_minor": 10000,
"already_refunded_minor": 2000,
"note": "Customer requested refund",
"merchant_case_id": "CASE-789"
}
}
Required Fields:
order_id: Order identifiercustomer_id: Customer identifieramount_minor: Refund amount in minor units (cents, yen, etc.)currency: Currency code (USD, EUR, GBP, JPY, etc.)region: Geographic region (US, EU, APAC, etc.)reason_code: Refund reason codeidempotency_key: Unique key for deduplication
Optional Fields:
order_currency: Original order currency (for cross-currency validation)order_total_minor: Original order total in minor unitsalready_refunded_minor: Previously refunded amountnote: Additional notesmerchant_case_id: Merchant's case identifier
Success Response (200):
{
"allow": true,
"decision_id": "dec_01HJ...7",
"expires_in": 60,
"remaining_daily_cap": {
"USD": 25000
},
"passport": {
"agent_id": "agents/your_agent_id",
"status": "active",
"assurance_level": "L2",
"capabilities": ["finance.payment.refund"],
"limits": {
"supported_currencies": ["USD", "EUR", "GBP"],
"currency_limits": {
"USD": {
"max_per_tx": 10000,
"daily_cap": 50000
}
},
"refund_reason_codes": ["customer_request", "defective", "not_as_described"]
},
"regions": ["US", "EU"]
}
}
Error Response (403):
{
"allow": false,
"reasons": [
{
"code": "daily_cap_exceeded",
"message": "Daily cap 50000 USD exceeded for USD; current 48000 + 5000 > 50000"
}
],
"decision_id": "dec_01HJ...8",
"remaining_daily_cap": {
"USD": 2000
}
}
Error Codes
| Code | Description | HTTP Status |
|---|---|---|
missing_required_field
|
Required field is missing | 400 |
invalid_amount
|
Amount validation failed | 400 |
currency_not_supported
|
Currency not supported | 400 |
invalid_idempotency_key
|
Invalid idempotency key format | 400 |
amount_exceeds_per_tx
|
Amount exceeds per-transaction limit | 403 |
daily_cap_exceeded
|
Daily spending cap exceeded | 403 |
assurance_too_low
|
Insufficient assurance level | 403 |
region_not_allowed
|
Region not allowed for agent | 403 |
reason_code_invalid
|
Invalid reason code | 403 |
idempotency_replay
|
Duplicate idempotency key | 403 |
order_balance_exceeded
|
Refund exceeds order balance | 403 |
cross_currency_denied
|
Cross-currency refunds not supported | 403 |
Assurance Level Requirements
| Amount Range | Required Assurance | Description |
|---|---|---|
| ⤠$100 USD | L2 | GitHub organization verification |
| $100 - $500 USD | L3 | Domain .well-known or TXT record |
| > $500 USD | L4 | Human handoff required (denied in v1) |
Note: Amounts are converted to USD equivalent for assurance calculation
Supported Currencies
| Currency | Code | Decimals | Minor Unit |
|---|---|---|---|
| US Dollar | USD | 2 | Cent |
| Euro | EUR | 2 | Cent |
| British Pound | GBP | 2 | Penny |
| Japanese Yen | JPY | 0 | Yen |
| Canadian Dollar | CAD | 2 | Cent |
| Australian Dollar | AUD | 2 | Cent |
| Swiss Franc | CHF | 2 | Rappen |
| Chinese Yuan | CNY | 2 | Fen |
| Indian Rupee | INR | 2 | Paisa |
| Brazilian Real | BRL | 2 | Centavo |
| Mexican Peso | MXN | 2 | Centavo |
Reason Codes
| Code | Description |
|---|---|
customer_request
|
Customer requested refund |
defective
|
Product was defective |
not_as_described
|
Product not as described |
duplicate
|
Duplicate charge |
fraud
|
Fraudulent transaction |
cancelled
|
Order was cancelled |
returned
|
Product was returned |
SDK Usage
Node.js SDK
const { verifyPolicy } = require('@aporthq/sdk-node');
// Basic usage
const result = await verifyPolicy('agents/your_agent_id', 'finance.payment.refund.v1', {
order_id: 'ORD-12345',
customer_id: 'CUST-67890',
amount_minor: 5000,
currency: 'USD',
region: 'US',
reason_code: 'customer_request',
idempotency_key: 'unique_key_123'
});
if (result.allowed) {
console.log('Refund approved:', result.result);
} else {
console.error('Refund denied:', result.error);
}
Python SDK
from aporthq_sdk_python import check_policy_compliance
# Basic usage
result = await check_policy_compliance(
agent_id="agents/your_agent_id",
policy_id="finance.payment.refund.v1",
context={
"order_id": "ORD-12345",
"customer_id": "CUST-67890",
"amount_minor": 5000,
"currency": "USD",
"region": "US",
"reason_code": "customer_request",
"idempotency_key": "unique_key_123"
}
)
if result["allowed"]:
print("Refund approved:", result["policy_result"])
else:
print("Refund denied:", result["error"])
Middleware Usage
Express.js Middleware
const express = require('express');
const { requireRefundsPolicy } = require('@aporthq/middleware-express');
const app = express();
app.use(express.json());
// Apply refunds policy to route
app.post('/refund', requireRefundsPolicy('agents/your_agent_id'), (req, res) => {
// Policy already verified, process refund
const policyResult = req.policyResult;
res.json({
success: true,
refund_id: 'ref_123',
decision_id: policyResult.evaluation.decision_id,
remaining_daily_cap: policyResult.evaluation.remaining_daily_cap
});
});
FastAPI Middleware
from fastapi import FastAPI, Depends
from aporthq_middleware_fastapi import require_refunds_policy
app = FastAPI()
@app.post("/refund")
async def process_refund(
policy_result = Depends(require_refunds_policy("agents/your_agent_id"))
):
# Policy already verified, process refund
return {
"success": True,
"refund_id": "ref_123",
"decision_id": policy_result.evaluation.decision_id,
"remaining_daily_cap": policy_result.evaluation.remaining_daily_cap
}
Monitoring and Audit
Verifiable Attestation
All policy decisions are logged with the following information:
- Decision ID
- Agent ID
- Timestamp
- Outcome (allow/deny)
- Reasons for denial
- Context data
- Cryptographic signature
Monitoring Endpoints
Get Verifiable Attestation: GET /api/owners/{owner_id}/audit
Get Alerts: GET /api/monitoring/alerts
Get Metrics: GET /api/metrics (admin only)
Example Verifiable Attestation Entry
{
"decision_id": "dec_01HJ...7",
"agent_id": "agents/your_agent_id",
"timestamp": "2024-01-16T10:30:00Z",
"outcome": "deny",
"reasons": [
{
"code": "daily_cap_exceeded",
"message": "Daily cap 50000 USD exceeded"
}
],
"context": {
"order_id": "ORD-12345",
"amount_minor": 5000,
"currency": "USD"
},
"action_hash": "sha256:...",
"registry_sig": "hmac:..."
}
Rate Limiting
Refunds are evaluated through POST /api/verify/policy/finance.payment.refund.v1,
which is on the policy verification tier: 100,000 requests a minute in preview and
200,000 in production, from POLICY_RATE_LIMIT_PER_MINUTE. Counted per client,
per route.
There is no per-agent-per-hour counter and no burst allowance; earlier versions of
this page described both, and neither exists in the limiter. Per-agent refund
volume is bounded by the passport's own daily cap
(limits.payments.refund.currency_limits[currency].daily_cap), which is a money
limit rather than a request limit and is enforced by the policy, not the limiter.
See docs/rate-limiting.md.
Caching
- Policy Cache: 60 seconds TTL
- Verification Cache: 60 seconds TTL
- Rate Limit Cache: 1 minute TTL
Security Considerations
- Idempotency Keys: Must be unique per agent/order combination
- Amount Validation: All amounts validated for precision and bounds
- Currency Support: Only supported currencies allowed
- Region Validation: Geographic restrictions enforced
- Verifiable Attestation: All decisions cryptographically signed
- Rate Limiting: DoS protection implemented
- Cross-Currency: Explicitly denied in v1
Migration from v0
If migrating from a previous version:
- Update SDK to latest version
- Add required fields to context
- Implement idempotency key generation
- Update error handling for new error codes
- Test with new assurance level requirements
Support
For questions or issues:
- Documentation: docs.aport.io
- Support: [email protected]
- GitHub: github.com/aporthq/agent-passport