Journey Docs
Members APITransfers

Cancel Pending Transfer

Cancel a pending transfer and return points to sender

POST/members/me/transfers/{transferId}/cancel

Overview

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

transferIdpathstringrequired

Pending 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

messagestringrequired

Success 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

StatusDescriptionReason
CLAIMEDRecipient has claimed the transferPoints already transferred
EXPIREDTransfer has passed expiration dateSystem auto-expired
CANCELLEDAlready cancelledCannot cancel twice
COMPLETEDDirect 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: cancelledAt field 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

On this page