Peer-to-peer payment apps like Venmo, Cash App, and Zelle use deep links to simplify money transfers. Instead of opening the app, finding the recipient, typing an amount, and adding a note, the user taps a single link and sees a pre-filled transfer form. This pattern reduces transfer friction and increases completion rates.
For the broader payment deep linking strategy, see Payment Deep Links. For the fintech overview, see Deep Linking for Fintech and Banking Apps.
The P2P Transfer Flow
Without Deep Links
- Open app
- Tap "Send Money"
- Search for recipient
- Enter amount
- Add note
- Confirm
- Authenticate (biometrics/PIN)
Seven steps. Each step is a potential drop-off.
With Deep Links
- Tap deep link
- Review pre-filled transfer (recipient, amount, note)
- Confirm
- Authenticate
Four steps. The user's decision to send money was already made when they tapped the link; the app just needs confirmation and authentication.
URL Structure
Request Payment
When a user wants to receive money:
https://go.yourapp.com/send?to=@janedoe&amount=25.00¬e=Coffee
Request Without Amount
Sometimes the recipient doesn't want to specify an amount (splitting a bill where each person's share varies):
https://go.yourapp.com/send?to=@janedoe¬e=Dinner+split
The sender fills in their own amount.
Request by Identifier Type
Different identifier types:
By username: /send?to=@janedoe
By phone: /send?to=+15551234567
By email: /[email protected]
By user ID: /send?to=uid:user_abc123
Group Payment Request
For splitting among multiple people:
https://go.yourapp.com/split?from=@janedoe&total=120.00&count=4¬e=Dinner
Each person owes $30.00. The link opens the app showing "Jane is requesting $30.00 for Dinner."
Implementation
Generating Payment Request Links
function createPaymentRequestLink(recipient, amount, note) {
const params = new URLSearchParams();
params.set('to', recipient.username);
if (amount) params.set('amount', amount.toFixed(2));
if (note) params.set('note', note);
return `https://go.yourapp.com/send?${params.toString()}`;
}
// Usage
const link = createPaymentRequestLink(
{ username: '@janedoe' },
25.00,
'Coffee'
);
// "https://go.yourapp.com/send?to=%40janedoe&amount=25.00¬e=Coffee"
Handling Payment Request Links
function handleSendDeepLink(url) {
const parsed = new URL(url);
const params = Object.fromEntries(parsed.searchParams);
// Require authentication
if (user.isAuthenticated === false) {
pendingDeepLink.save(url);
navigation.navigate('Auth');
return;
}
// Resolve recipient
const recipientId = params.to;
const recipient = await resolveRecipient(recipientId);
if (recipient === null) {
navigation.navigate('RecipientNotFound', { identifier: recipientId });
return;
}
// Parse amount
const amount = params.amount ? parseFloat(params.amount) : null;
// Navigate to send screen
navigation.navigate('SendMoney', {
recipient: recipient,
amount: amount,
note: params.note || '',
isPreFilled: true,
});
}
Recipient Resolution
Resolve the identifier to a real user:
async function resolveRecipient(identifier) {
if (identifier.startsWith('@')) {
return await lookupByUsername(identifier.substring(1));
} else if (identifier.startsWith('+')) {
return await lookupByPhone(identifier);
} else if (identifier.includes('@')) {
return await lookupByEmail(identifier);
} else if (identifier.startsWith('uid:')) {
return await lookupByUserId(identifier.substring(4));
}
return null;
}
Security Considerations
Recipient Verification
Before showing the transfer form, verify the recipient is real and active:
const recipient = await resolveRecipient(params.to);
if (recipient === null) {
showError('This user was not found.');
return;
}
if (recipient.accountStatus !== 'active') {
showError('This user cannot receive payments at this time.');
return;
}
// Show recipient details for the user to verify
// "Send $25.00 to Jane Doe (@janedoe)?"
Display the recipient's full name and profile photo so the sender can verify they're sending to the right person.
Amount Limits
Validate amounts against transaction limits:
const amount = parseFloat(params.amount);
if (isNaN(amount) || amount <= 0) {
showError('Invalid amount');
return;
}
if (amount > user.dailyTransferLimit) {
showError(`Amount exceeds your daily transfer limit of $${user.dailyTransferLimit}`);
return;
}
if (amount > user.balance) {
navigation.navigate('InsufficientFunds', {
required: amount,
available: user.balance,
});
return;
}
Preventing Impersonation
A malicious actor could create a link with someone else's username as the recipient:
"Send me $100 for the tickets!"
https://go.yourapp.com/send?to=@attacker&amount=100.00¬e=Concert+tickets
The "to" field shows "@attacker" instead of the real friend. To prevent this:
- Always show the full recipient profile: Name, photo, username. The sender should recognize the recipient.
- Contacts integration: If the recipient is in the sender's contacts, show the contact name alongside the username.
- Warning for unknown recipients: If the sender has never transacted with this recipient before, show a warning: "You haven't sent money to this person before. Make sure you recognize them."
Link Expiration
Payment request links should expire:
https://go.yourapp.com/send?to=@janedoe&amount=25.00¬e=Coffee&exp=2026-05-01
After the expiration date, the app shows "This payment request has expired" instead of the transfer form. This prevents stale requests from being paid accidentally.
Sharing Payment Request Links
In-App Share Sheet
import { Share } from 'react-native';
async function sharePaymentRequest(amount, note) {
const link = createPaymentRequestLink(user, amount, note);
Share.share({
message: `${user.displayName} is requesting $${amount.toFixed(2)} for "${note}". Pay here: ${link}`,
});
}
QR Code
Generate a QR code for in-person payment requests:
// Display QR code in the app for another user to scan
const qrData = `https://go.yourapp.com/send?to=@${user.username}&amount=25.00¬e=Coffee`;
NFC
For contactless payment requests, encode the deep link in an NFC tag. The payer taps their phone against the tag, and the transfer form opens.
OG Metadata for Payment Links
When shared in messaging apps:
og:title: Jane is requesting $25.00
og:description: For "Coffee" via [Your App]
og:image: payment-request-card.png (branded payment request visual)
This makes the payment request look professional and trustworthy in message previews.
Deferred P2P Deep Links
If the payer doesn't have the app:
- Payer taps the link
- Landing page shows: "Jane is requesting $25.00 for Coffee"
- Landing page has an "Install App" button and a "Pay on Web" option
- If the payer installs the app, deferred deep linking routes them to the pre-filled transfer form on first launch
The deferred deep link ensures the payment context survives the install process.
Measuring Performance
| Metric | Description |
|---|---|
| Request share rate | % of users who share payment request links |
| Link tap rate | % of shared links that are tapped |
| Authentication rate | % of link taps that authenticate |
| Transfer completion rate | % of authenticated sessions that complete the transfer |
| Average transfer amount | Mean $ value of transfers from deep links |
| Time to complete | Average seconds from link tap to transfer confirmed |
For deep linking features, see Tolinku deep linking. For passing data through deep links, see Deep Link Parameters.
Get deep linking tips in your inbox
One email per week. No spam.