Members APITransfers
Lookup Member
Lookup member before transferring points
POST
/members/me/transfers/lookupOverview
Lookup a member by email, phone, or member code before transferring points. Returns minimal public information to protect member privacy while allowing senders to verify they have the correct recipient.
Authentication
Requires a valid Clerk JWT token in the Authorization header.
Rate Limiting
Rate Limited: 25 requests per hour per member to prevent abuse and protect member privacy.
Request Body
querybodystringrequiredSearch query - email address, phone number, or member code
typebodystringrequiredType of lookup: "email", "phone", or "memberCode"
Request
curl -X POST "http://localhost:3000/v1/members/me/transfers/lookup" \
-H "Authorization: Bearer YOUR_CLERK_JWT" \
-H "Content-Type: application/json" \
-d '{
"query": "friend@example.com",
"type": "email"
}'Response
foundbooleanrequiredWhether a member was found matching the query
memberobjectMember information (only present if found is true)
member object
displayNamestringrequiredMember's display name for verification
avatarUrlstringMember's avatar URL (null if no avatar)
externalIdstringrequiredMember's external ID (ULID) for use in transfer creation
Response
{
"found": true,
"member": {
"displayName": "John Smith",
"avatarUrl": "https://cdn.example.com/avatars/john-smith.jpg",
"externalId": "01ARZ3NDEKTSV4RRFFQ69G5FAV"
}
}Lookup Types
Email Lookup
- Query Format: Valid email address
- Example:
friend@example.com - Case Sensitivity: Case insensitive
- Privacy: Only finds verified email addresses
Phone Lookup
- Query Format: E.164 format phone number
- Example:
+1234567890 - Formatting: Country code required
- Privacy: Only finds verified phone numbers
Member Code Lookup
- Query Format: Member's unique code
- Example:
MEMBER123or custom format - Case Sensitivity: Case insensitive
- Usage: Public member codes for easy sharing
Privacy Protection
Limited Information
- Display Name: Only public display name shown
- Avatar: Public avatar URL (if set)
- No Personal Data: Email, phone, addresses not exposed
- External ID: Safe identifier for transfers
Rate Limiting
- Limit: 25 requests per hour per member
- Purpose: Prevent mass harvesting of member information
- Reset: Rolling 1-hour window
- Bypass: Not available (security measure)
Self-Lookup Prevention
- Blocked: Cannot lookup your own account
- Error: Returns specific error message
- Purpose: Prevent self-transfer attempts
Use Cases
Transfer Confirmation UI
const confirmTransferRecipient = async (email: string) => {
try {
const response = await fetch('/api/members/me/transfers/lookup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: email,
type: 'email'
})
});
const result = await response.json();
if (result.found) {
// Show confirmation dialog
const confirmed = confirm(
`Send points to ${result.member.displayName}?`
);
if (confirmed) {
// Proceed with transfer using result.member.externalId
return result.member.externalId;
}
} else {
// Offer to send as gift transfer
const sendAsGift = confirm(
'Member not found. Send as gift transfer instead?'
);
if (sendAsGift) {
return null; // Use email in gift transfer
}
}
} catch (error) {
console.error('Lookup failed:', error);
}
};Auto-Complete Search
const searchMembers = async (query: string, type: 'email' | 'phone' | 'memberCode') => {
// Debounce to respect rate limits
if (query.length < 3) return [];
try {
const response = await fetch('/api/members/me/transfers/lookup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query, type })
});
const result = await response.json();
if (result.found) {
return [{
id: result.member.externalId,
name: result.member.displayName,
avatar: result.member.avatarUrl,
type: 'member'
}];
}
// Suggest gift transfer option
return [{
id: null,
name: `Send as gift to ${query}`,
avatar: null,
type: 'gift'
}];
} catch (error) {
return [];
}
};Batch Lookup (Rate-Limited)
const lookupMultipleMembers = async (contacts: Array<{query: string, type: string}>) => {
const results = [];
const DELAY_MS = 150; // Respect rate limits
for (const contact of contacts) {
try {
const response = await fetch('/api/members/me/transfers/lookup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(contact)
});
const result = await response.json();
results.push({ ...contact, ...result });
// Delay between requests
await new Promise(resolve => setTimeout(resolve, DELAY_MS));
} catch (error) {
results.push({ ...contact, found: false, error: true });
}
}
return results;
};Error Handling
Rate Limit Management
const lookupWithRetry = async (query: string, type: string, maxRetries = 2) => {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const response = await fetch('/api/members/me/transfers/lookup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query, type })
});
if (response.status === 429) {
// Rate limited - wait and retry
if (attempt < maxRetries) {
const waitTime = Math.pow(2, attempt) * 1000; // Exponential backoff
await new Promise(resolve => setTimeout(resolve, waitTime));
continue;
}
throw new Error('Rate limit exceeded');
}
return response.json();
} catch (error) {
if (attempt === maxRetries) throw error;
}
}
};Validation
Query Format Validation
const validateLookupQuery = (query: string, type: string) => {
const errors = [];
if (!query || query.trim().length === 0) {
errors.push('Query is required');
}
if (query.length > 255) {
errors.push('Query too long (max 255 characters)');
}
switch (type) {
case 'email':
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(query)) {
errors.push('Invalid email format');
}
break;
case 'phone':
if (!/^\+?[1-9]\d{1,14}$/.test(query)) {
errors.push('Invalid phone format (use E.164)');
}
break;
case 'memberCode':
if (!/^[A-Z0-9]{6,20}$/i.test(query)) {
errors.push('Invalid member code format');
}
break;
default:
errors.push('Invalid lookup type');
}
return errors;
};Related Endpoints
- Create Transfer - Use external ID from lookup for direct transfers
- Get Transfer Limits - Check limits before showing transfer options