Members APITransfers
Cancel Pending Transfer
Cancel a pending transfer and return points to sender
POST
/members/me/transfers/{transferId}/cancelOverview
Cancels a pending transfer and returns the points to the sender's account. Only the sender can cancel their own pending transfers. This action is irreversible and immediately returns the points to your available balance.
Authentication
Requires a valid Clerk JWT token in the Authorization header.
Security
Reverification Required: This endpoint requires reverification if the user hasn't verified their credentials within the last 10 minutes. This is a sensitive action involving point transfers.
Path Parameters
transferIdpathstringrequiredPending transfer ID to cancel
Request
curl -X POST "http://localhost:3000/v1/members/me/transfers/123e4567-e89b-12d3-a456-426614174000/cancel" \
-H "Authorization: Bearer YOUR_CLERK_JWT"Response
messagestringrequiredSuccess message confirming the cancellation
Response
{
"message": "Transfer cancelled successfully"
}Cancellation Rules
Who Can Cancel
- Sender Only: Only the member who created the transfer
- No Recipient Cancellation: Recipients cannot cancel incoming transfers
- Authorization: Verified through member ID matching
When You Can Cancel
- Status: Only transfers with status "PENDING"
- Not Claimed: Transfer has not been claimed by recipient
- Not Expired: Transfer has not passed expiration date
- Before Completion: Cannot cancel completed transfers
When You Cannot Cancel
| Status | Description | Reason |
|---|---|---|
| CLAIMED | Recipient has claimed the transfer | Points already transferred |
| EXPIRED | Transfer has passed expiration date | System auto-expired |
| CANCELLED | Already cancelled | Cannot cancel twice |
| COMPLETED | Direct transfer (non-pending) | Immediate completion |
What Happens When You Cancel
Points Return
- Immediate: Points returned to sender's account instantly
- Full Amount: Complete original transfer amount returned
- No Fees: No cancellation fees or penalties
- Balance Update: Wallet balance updated in real-time
Transfer Status
- Status Change: Transfer marked as CANCELLED
- Timestamp:
cancelledAtfield set to current time - Immutable: Cannot uncanccel once cancelled
- Audit Trail: Cancellation recorded for both parties
Notifications
- Sender: Confirmation of successful cancellation
- Recipient: Notification that gift was cancelled (if they have an account)
- Email/SMS: Notification sent to pending transfer contact
Use Cases
Change of Mind
// Cancel a transfer you no longer want to send
const cancelGiftTransfer = async (transferId: string) => {
try {
const response = await fetch(`/api/members/me/transfers/${transferId}/cancel`, {
method: 'POST'
});
if (response.ok) {
const result = await response.json();
console.log('Transfer cancelled:', result.message);
// Refresh pending transfers list
refreshPendingTransfers();
// Refresh wallet balance
refreshWalletBalance();
// Show success message
showNotification('Gift transfer cancelled. Points returned to your account.');
}
} catch (error) {
console.error('Failed to cancel transfer:', error);
}
};Bulk Cancellation
// Cancel multiple pending transfers
const cancelMultipleTransfers = async (transferIds: string[]) => {
const results = [];
for (const transferId of transferIds) {
try {
const response = await fetch(`/api/members/me/transfers/${transferId}/cancel`, {
method: 'POST'
});
const result = await response.json();
results.push({
transferId,
success: response.ok,
message: result.message
});
} catch (error) {
results.push({
transferId,
success: false,
error: error.message
});
}
}
const successful = results.filter(r => r.success).length;
console.log(`Cancelled ${successful}/${transferIds.length} transfers`);
return results;
};Expiry Management
// Cancel transfers that are about to expire
const cancelExpiringTransfers = async () => {
const pendingTransfers = await getPendingTransfers('sent');
const expiringThreshold = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24 hours
const expiringTransfers = pendingTransfers.items.filter(transfer => {
const expiryDate = new Date(transfer.expiresAt);
return expiryDate <= expiringThreshold && transfer.status === 'PENDING';
});
if (expiringTransfers.length > 0) {
const shouldCancel = confirm(
`You have ${expiringTransfers.length} transfers expiring soon. Cancel them to recover points?`
);
if (shouldCancel) {
const results = await cancelMultipleTransfers(
expiringTransfers.map(t => t.transferId)
);
const totalRecovered = expiringTransfers
.filter((_, index) => results[index].success)
.reduce((sum, transfer) => sum + transfer.amount, 0);
showNotification(`Recovered ${totalRecovered} points from ${results.filter(r => r.success).length} cancelled transfers`);
}
}
};Error Handling
Common Cancellation Scenarios
const handleCancellationError = (error: any, transferId: string) => {
switch (error.error) {
case 'TRANSFER_NOT_CANCELLABLE':
if (error.message.includes('claimed')) {
showMessage('This transfer has already been claimed by the recipient.');
} else if (error.message.includes('expired')) {
showMessage('This transfer has expired and cannot be cancelled.');
} else {
showMessage('This transfer cannot be cancelled.');
}
break;
case 'UNAUTHORIZED':
showMessage('You can only cancel transfers that you sent.');
break;
case 'NOT_FOUND':
showMessage('Transfer not found. It may have already been processed.');
break;
default:
showMessage('Unable to cancel transfer. Please try again later.');
}
};Retry Logic
const cancelWithRetry = async (transferId: string, maxRetries = 2) => {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(`/api/members/me/transfers/${transferId}/cancel`, {
method: 'POST'
});
if (response.ok) {
return await response.json();
}
if (response.status === 403 && attempt < maxRetries) {
// Might be reverification required - wait and retry
await new Promise(resolve => setTimeout(resolve, 1000));
continue;
}
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
} catch (error) {
if (attempt === maxRetries) throw error;
// Wait before retry
await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 1000));
}
}
};Security Considerations
Reverification
- Requirement: Must reverify within 10 minutes
- Methods: SMS OTP or email verification
- Purpose: Prevents unauthorized cancellations
- Timeout: Strict timeframe enforcement
Authorization Checks
- Sender Verification: Only transfer creator can cancel
- Member ID Match: Server validates ownership
- No Delegation: Cannot cancel on behalf of others
- Audit Logging: All attempts logged for security
Rate Limiting
- No Specific Limits: Actions are naturally limited by transfer count
- Reverification: Acts as natural rate limiting
- Monitoring: Unusual patterns flagged for review
Impact on Other Systems
Wallet Balance
- Immediate Update: Balance increased by cancelled amount
- Transaction Record: Cancellation recorded as credit transaction
- Available Points: Immediately available for new transfers
Notifications
- Real-time: Cancellation notifications sent immediately
- Multiple Channels: In-app, email, SMS (if configured)
- Recipient Notice: Recipient informed gift was cancelled
Related Endpoints
- List Pending Transfers - View transfers available for cancellation
- Create Transfer - Create new transfers
- Get Transfer Limits - Check available transfer capacity after cancellation
- Get Wallet Balance - View updated balance after cancellation