Generate Login Link
curl --request POST \
--url https://api.fifteenth.com/v1beta/accounts/{account_id}/login-link \
--header 'Authorization: <authorization>' \
--header 'Content-Type: <content-type>' \
--data '
{
"user_email": "<string>",
"expiry_hours": 123,
"purpose": "<string>",
"redirect_url": "<string>",
"send_notification": true,
"notification_settings": {
"notification_settings.partner_branded": true,
"notification_settings.custom_subject": "<string>",
"notification_settings.custom_message": "<string>",
"notification_settings.include_instructions": true
}
}
'import requests
url = "https://api.fifteenth.com/v1beta/accounts/{account_id}/login-link"
payload = {
"user_email": "<string>",
"expiry_hours": 123,
"purpose": "<string>",
"redirect_url": "<string>",
"send_notification": True,
"notification_settings": {
"notification_settings.partner_branded": True,
"notification_settings.custom_subject": "<string>",
"notification_settings.custom_message": "<string>",
"notification_settings.include_instructions": True
}
}
headers = {
"Authorization": "<authorization>",
"Content-Type": "<content-type>"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: '<authorization>', 'Content-Type': '<content-type>'},
body: JSON.stringify({
user_email: '<string>',
expiry_hours: 123,
purpose: '<string>',
redirect_url: '<string>',
send_notification: true,
notification_settings: {
'notification_settings.partner_branded': true,
'notification_settings.custom_subject': '<string>',
'notification_settings.custom_message': '<string>',
'notification_settings.include_instructions': true
}
})
};
fetch('https://api.fifteenth.com/v1beta/accounts/{account_id}/login-link', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.fifteenth.com/v1beta/accounts/{account_id}/login-link",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'user_email' => '<string>',
'expiry_hours' => 123,
'purpose' => '<string>',
'redirect_url' => '<string>',
'send_notification' => true,
'notification_settings' => [
'notification_settings.partner_branded' => true,
'notification_settings.custom_subject' => '<string>',
'notification_settings.custom_message' => '<string>',
'notification_settings.include_instructions' => true
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: <authorization>",
"Content-Type: <content-type>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.fifteenth.com/v1beta/accounts/{account_id}/login-link"
payload := strings.NewReader("{\n \"user_email\": \"<string>\",\n \"expiry_hours\": 123,\n \"purpose\": \"<string>\",\n \"redirect_url\": \"<string>\",\n \"send_notification\": true,\n \"notification_settings\": {\n \"notification_settings.partner_branded\": true,\n \"notification_settings.custom_subject\": \"<string>\",\n \"notification_settings.custom_message\": \"<string>\",\n \"notification_settings.include_instructions\": true\n }\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "<authorization>")
req.Header.Add("Content-Type", "<content-type>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.fifteenth.com/v1beta/accounts/{account_id}/login-link")
.header("Authorization", "<authorization>")
.header("Content-Type", "<content-type>")
.body("{\n \"user_email\": \"<string>\",\n \"expiry_hours\": 123,\n \"purpose\": \"<string>\",\n \"redirect_url\": \"<string>\",\n \"send_notification\": true,\n \"notification_settings\": {\n \"notification_settings.partner_branded\": true,\n \"notification_settings.custom_subject\": \"<string>\",\n \"notification_settings.custom_message\": \"<string>\",\n \"notification_settings.include_instructions\": true\n }\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.fifteenth.com/v1beta/accounts/{account_id}/login-link")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = '<authorization>'
request["Content-Type"] = '<content-type>'
request.body = "{\n \"user_email\": \"<string>\",\n \"expiry_hours\": 123,\n \"purpose\": \"<string>\",\n \"redirect_url\": \"<string>\",\n \"send_notification\": true,\n \"notification_settings\": {\n \"notification_settings.partner_branded\": true,\n \"notification_settings.custom_subject\": \"<string>\",\n \"notification_settings.custom_message\": \"<string>\",\n \"notification_settings.include_instructions\": true\n }\n}"
response = http.request(request)
puts response.read_body{
"login_link": "<string>",
"user_email": "<string>",
"expires_at": "<string>",
"expires_in_hours": 123,
"purpose": "<string>",
"redirect_url": "<string>",
"notification_sent": true,
"previous_links_invalidated": 123,
"created_at": "<string>",
"account_id": 123,
"404 Not Found": {},
"400 Bad Request": {},
"403 Forbidden": {},
"429 Too Many Requests": {}
}Accounts
Generate Login Link
Generate a new secure login link for client access to their Fifteenth account
POST
/
v1beta
/
accounts
/
{account_id}
/
login-link
Generate Login Link
curl --request POST \
--url https://api.fifteenth.com/v1beta/accounts/{account_id}/login-link \
--header 'Authorization: <authorization>' \
--header 'Content-Type: <content-type>' \
--data '
{
"user_email": "<string>",
"expiry_hours": 123,
"purpose": "<string>",
"redirect_url": "<string>",
"send_notification": true,
"notification_settings": {
"notification_settings.partner_branded": true,
"notification_settings.custom_subject": "<string>",
"notification_settings.custom_message": "<string>",
"notification_settings.include_instructions": true
}
}
'import requests
url = "https://api.fifteenth.com/v1beta/accounts/{account_id}/login-link"
payload = {
"user_email": "<string>",
"expiry_hours": 123,
"purpose": "<string>",
"redirect_url": "<string>",
"send_notification": True,
"notification_settings": {
"notification_settings.partner_branded": True,
"notification_settings.custom_subject": "<string>",
"notification_settings.custom_message": "<string>",
"notification_settings.include_instructions": True
}
}
headers = {
"Authorization": "<authorization>",
"Content-Type": "<content-type>"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: '<authorization>', 'Content-Type': '<content-type>'},
body: JSON.stringify({
user_email: '<string>',
expiry_hours: 123,
purpose: '<string>',
redirect_url: '<string>',
send_notification: true,
notification_settings: {
'notification_settings.partner_branded': true,
'notification_settings.custom_subject': '<string>',
'notification_settings.custom_message': '<string>',
'notification_settings.include_instructions': true
}
})
};
fetch('https://api.fifteenth.com/v1beta/accounts/{account_id}/login-link', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.fifteenth.com/v1beta/accounts/{account_id}/login-link",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'user_email' => '<string>',
'expiry_hours' => 123,
'purpose' => '<string>',
'redirect_url' => '<string>',
'send_notification' => true,
'notification_settings' => [
'notification_settings.partner_branded' => true,
'notification_settings.custom_subject' => '<string>',
'notification_settings.custom_message' => '<string>',
'notification_settings.include_instructions' => true
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: <authorization>",
"Content-Type: <content-type>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.fifteenth.com/v1beta/accounts/{account_id}/login-link"
payload := strings.NewReader("{\n \"user_email\": \"<string>\",\n \"expiry_hours\": 123,\n \"purpose\": \"<string>\",\n \"redirect_url\": \"<string>\",\n \"send_notification\": true,\n \"notification_settings\": {\n \"notification_settings.partner_branded\": true,\n \"notification_settings.custom_subject\": \"<string>\",\n \"notification_settings.custom_message\": \"<string>\",\n \"notification_settings.include_instructions\": true\n }\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "<authorization>")
req.Header.Add("Content-Type", "<content-type>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.fifteenth.com/v1beta/accounts/{account_id}/login-link")
.header("Authorization", "<authorization>")
.header("Content-Type", "<content-type>")
.body("{\n \"user_email\": \"<string>\",\n \"expiry_hours\": 123,\n \"purpose\": \"<string>\",\n \"redirect_url\": \"<string>\",\n \"send_notification\": true,\n \"notification_settings\": {\n \"notification_settings.partner_branded\": true,\n \"notification_settings.custom_subject\": \"<string>\",\n \"notification_settings.custom_message\": \"<string>\",\n \"notification_settings.include_instructions\": true\n }\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.fifteenth.com/v1beta/accounts/{account_id}/login-link")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = '<authorization>'
request["Content-Type"] = '<content-type>'
request.body = "{\n \"user_email\": \"<string>\",\n \"expiry_hours\": 123,\n \"purpose\": \"<string>\",\n \"redirect_url\": \"<string>\",\n \"send_notification\": true,\n \"notification_settings\": {\n \"notification_settings.partner_branded\": true,\n \"notification_settings.custom_subject\": \"<string>\",\n \"notification_settings.custom_message\": \"<string>\",\n \"notification_settings.include_instructions\": true\n }\n}"
response = http.request(request)
puts response.read_body{
"login_link": "<string>",
"user_email": "<string>",
"expires_at": "<string>",
"expires_in_hours": 123,
"purpose": "<string>",
"redirect_url": "<string>",
"notification_sent": true,
"previous_links_invalidated": 123,
"created_at": "<string>",
"account_id": 123,
"404 Not Found": {},
"400 Bad Request": {},
"403 Forbidden": {},
"429 Too Many Requests": {}
}Overview
The Generate Login Link endpoint creates a new secure, time-limited login link for clients to access their Fifteenth account. This is useful when:- The original login link has expired (7-day expiry)
- A client has lost their login link
- You need to provide additional login links to multiple users
- Security requires generating a fresh login link
Each login link is single-use for initial access but allows the client to set up permanent login credentials once used.
Request
Path Parameters
number
required
The unique Fifteenth account identifier to generate a login link for.Format: Numeric ID
Example:
Example:
12345Headers
string
required
Bearer token with your Partner API key
string
required
Must be
application/jsonBody Parameters
string
Email address of the specific user to generate a login link for. If not provided, generates a link for the primary account holder.Validation: Must be an existing user on the account
Example:
Example:
john.doe@example.cominteger
Number of hours until the login link expires.Range: 1-168 hours (1 hour to 7 days)
Default: 168 hours (7 days)
Example:
Default: 168 hours (7 days)
Example:
48string
Purpose for generating the login link (for audit logging).Options:
initial_access- First time account accessexpired_link- Replacing an expired linklost_credentials- Client lost their login informationadditional_user- Link for additional account usersupport_request- Generated for customer support
expired_linkstring
URL to redirect the client to after successful login. Must be a permitted domain for your partner account.Format: Valid HTTPS URL
Example:
Example:
https://yourplatform.com/tax-dashboardboolean
Whether to send an email notification to the user with the new login link.Default:
trueobject
Email notification customization options.
Show notification_settings object
Show notification_settings object
Response
Success Response
string
The generated secure login link for the client.Format:
Security: Token is cryptographically secure and single-use for initial access
https://app.fifteenth.com/auth/partner-login/{token}Security: Token is cryptographically secure and single-use for initial access
string
Email address of the user this login link is for.
string
ISO 8601 timestamp when the login link expires.
integer
Number of hours until the login link expires.
string
The purpose specified for generating this login link.
string
URL the client will be redirected to after login (if specified).
boolean
Whether an email notification was sent to the user.
integer
Number of previous login links that were invalidated when generating this new one.
string
ISO 8601 timestamp when the login link was generated.
number
The account ID this login link provides access to.
Examples
Basic Login Link Generation
import requests
account_id = 12345
url = f"https://api.fifteenth.com/v1beta/accounts/{account_id}/login-link"
headers = {
"Authorization": "Bearer 15th_your_api_key_here",
"Content-Type": "application/json"
}
# Generate login link with default settings
response = requests.post(url, headers=headers, json={})
link_data = response.json()
print(f"Login Link: {link_data['login_link']}")
print(f"Expires: {link_data['expires_at']}")
print(f"Notification Sent: {link_data['notification_sent']}")
const accountId = 12345;
const url = `https://api.fifteenth.com/v1beta/accounts/${accountId}/login-link`;
const headers = {
'Authorization': 'Bearer 15th_your_api_key_here',
'Content-Type': 'application/json'
};
// Generate login link with default settings
const response = await fetch(url, {
method: 'POST',
headers: headers,
body: JSON.stringify({})
});
const linkData = await response.json();
console.log(`Login Link: ${linkData.login_link}`);
console.log(`Expires: ${linkData.expires_at}`);
console.log(`Notification Sent: ${linkData.notification_sent}`);
curl -X POST "https://api.fifteenth.com/v1beta/accounts/12345/login-link" \
-H "Authorization: Bearer 15th_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{}'
Custom Login Link with Settings
data = {
"user_email": "john.doe@example.com",
"expiry_hours": 48, # 2 days
"purpose": "support_request",
"redirect_url": "https://yourplatform.com/tax-dashboard",
"send_notification": True,
"notification_settings": {
"partner_branded": True,
"custom_subject": "New Access Link for Your Tax Account",
"custom_message": "We've generated a new login link at your request. This link expires in 48 hours for security.",
"include_instructions": True
}
}
response = requests.post(url, headers=headers, json=data)
link_data = response.json()
print(f"Custom Login Link Generated:")
print(f"- Link: {link_data['login_link']}")
print(f"- Expires in: {link_data['expires_in_hours']} hours")
print(f"- Redirect: {link_data['redirect_url']}")
print(f"- Purpose: {link_data['purpose']}")
const data = {
user_email: 'john.doe@example.com',
expiry_hours: 48, // 2 days
purpose: 'support_request',
redirect_url: 'https://yourplatform.com/tax-dashboard',
send_notification: true,
notification_settings: {
partner_branded: true,
custom_subject: 'New Access Link for Your Tax Account',
custom_message: "We've generated a new login link at your request. This link expires in 48 hours for security.",
include_instructions: true
}
};
const response = await fetch(url, {
method: 'POST',
headers: headers,
body: JSON.stringify(data)
});
const linkData = await response.json();
console.log('Custom Login Link Generated:');
console.log(`- Link: ${linkData.login_link}`);
console.log(`- Expires in: ${linkData.expires_in_hours} hours`);
console.log(`- Redirect: ${linkData.redirect_url}`);
console.log(`- Purpose: ${linkData.purpose}`);
Generate for Specific User
# Generate login link for a specific user (like an accountant)
data = {
"user_email": "accountant@clientcompany.com",
"expiry_hours": 24, # 1 day for security
"purpose": "additional_user",
"send_notification": True,
"notification_settings": {
"custom_message": "Your client has granted you access to their Fifteenth account for the 2024 tax preparation."
}
}
response = requests.post(url, headers=headers, json=data)
link_data = response.json()
print(f"Generated link for: {link_data['user_email']}")
print(f"Link expires: {link_data['expires_at']}")
// Generate login link for a specific user (like an accountant)
const data = {
user_email: 'accountant@clientcompany.com',
expiry_hours: 24, // 1 day for security
purpose: 'additional_user',
send_notification: true,
notification_settings: {
custom_message: 'Your client has granted you access to their Fifteenth account for the 2024 tax preparation.'
}
};
const response = await fetch(url, {
method: 'POST',
headers: headers,
body: JSON.stringify(data)
});
const linkData = await response.json();
console.log(`Generated link for: ${linkData.user_email}`);
console.log(`Link expires: ${linkData.expires_at}`);
Silent Link Generation (No Email)
# Generate link without sending email (for manual delivery)
data = {
"expiry_hours": 72, # 3 days
"purpose": "manual_delivery",
"send_notification": False
}
response = requests.post(url, headers=headers, json=data)
link_data = response.json()
print(f"Login Link (for manual delivery): {link_data['login_link']}")
print(f"No notification sent: {not link_data['notification_sent']}")
# You would now deliver this link through your secure channel
secure_delivery_system.send_link(
recipient=client_email,
link=link_data['login_link'],
expires_at=link_data['expires_at']
)
// Generate link without sending email (for manual delivery)
const data = {
expiry_hours: 72, // 3 days
purpose: 'manual_delivery',
send_notification: false
};
const response = await fetch(url, {
method: 'POST',
headers: headers,
body: JSON.stringify(data)
});
const linkData = await response.json();
console.log(`Login Link (for manual delivery): ${linkData.login_link}`);
console.log(`No notification sent: ${!linkData.notification_sent}`);
// You would now deliver this link through your secure channel
secureDeliverySystem.sendLink({
recipient: clientEmail,
link: linkData.login_link,
expiresAt: linkData.expires_at
});
Response Examples
Basic Link Generation Response
Response
{
"login_link": "https://app.fifteenth.com/auth/partner-login/tk_A7xM9pQ2vK8xLqR4S1E6B3F",
"user_email": "john.doe@example.com",
"expires_at": "2024-01-22T10:30:00Z",
"expires_in_hours": 168,
"purpose": "expired_link",
"redirect_url": null,
"notification_sent": true,
"previous_links_invalidated": 1,
"created_at": "2024-01-15T10:30:00Z",
"account_id": "12345"
}
Custom Link Generation Response
Response (Custom Settings)
{
"login_link": "https://app.fifteenth.com/auth/partner-login/tk_M4pQ7vN9xR2mK5wL8S1E6B",
"user_email": "john.doe@example.com",
"expires_at": "2024-01-17T10:30:00Z",
"expires_in_hours": 48,
"purpose": "support_request",
"redirect_url": "https://yourplatform.com/tax-dashboard",
"notification_sent": true,
"previous_links_invalidated": 2,
"created_at": "2024-01-15T10:30:00Z",
"account_id": 12345
}
Error Responses
object
Account not found or user email not associated with account.
{
"error": {
"code": "ACCOUNT_NOT_FOUND",
"message": "Account not found",
"details": {
"account_id": "12345"
}
}
}
{
"error": {
"code": "USER_NOT_FOUND",
"message": "User email not found on this account",
"details": {
"user_email": "nonexistent@example.com",
"account_id": "12345"
}
}
}
object
Invalid request parameters.
{
"error": {
"code": "INVALID_EXPIRY",
"message": "Expiry hours must be between 1 and 168",
"details": {
"provided_hours": 200,
"min_hours": 1,
"max_hours": 168
}
}
}
{
"error": {
"code": "INVALID_REDIRECT_URL",
"message": "Redirect URL is not permitted for your partner account",
"details": {
"provided_url": "https://untrusted.com/dashboard",
"permitted_domains": ["yourplatform.com", "yourapp.net"]
}
}
}
object
Account access denied or suspended.
{
"error": {
"code": "ACCOUNT_SUSPENDED",
"message": "Cannot generate login links for suspended accounts",
"details": {
"account_id": "12345",
"status": "suspended",
"contact_support": true
}
}
}
object
Too many login link generation requests.
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Too many login link generation requests",
"details": {
"limit": 10,
"window_minutes": 60,
"reset_at": "2024-01-15T11:30:00Z"
}
}
}
Security Considerations
Link Security Features
Login links provide full account access. Handle them with the same security as passwords:
- Single-use for initial access: After first use, clients set up permanent credentials
- Time-limited: Links automatically expire (default 7 days, max 7 days)
- Cryptographically secure: Uses secure random token generation
- Invalidation: Previous links are automatically invalidated when new ones are generated
- Audit logging: All link generation and usage is logged for security
Best Practices
Expiry Time Selection
Expiry Time Selection
Choose appropriate expiry times based on urgency:
- Immediate support: 1-4 hours
- Normal client access: 24-48 hours
- Account setup: 7 days (default)
- High security situations: 1 hour minimum
Notification Security
Notification Security
When sending notifications:
- Always use partner branding for legitimacy
- Include expiry information in emails
- Warn clients not to share links
- Use custom messages to provide context
Manual Delivery
Manual Delivery
For sensitive situations:
- Set
send_notification: false - Deliver links through secure channels
- Use shorter expiry times
- Track delivery method for your records
Rate Limiting
Rate Limiting
Respect rate limits to prevent abuse:
- Max 10 links per account per hour
- Use purpose field for audit trails
- Monitor link generation patterns
- Implement client-side rate limiting
Monitoring and Alerts
Monitor login link usage for security:def monitor_login_link_security(account_id, link_data):
"""Monitor login link generation for security patterns"""
# Alert on rapid link generation
if link_data['previous_links_invalidated'] > 3:
security_alert(
f"Multiple login links generated rapidly for account {account_id}",
severity="medium"
)
# Alert on short expiry links (potential support issue)
if link_data['expires_in_hours'] < 4:
operational_alert(
f"Short-expiry login link generated: {link_data['expires_in_hours']} hours",
context=link_data['purpose']
)
# Log for audit trail
audit_log({
'event': 'login_link_generated',
'account_id': account_id,
'purpose': link_data['purpose'],
'expiry': link_data['expires_at'],
'notification_sent': link_data['notification_sent']
})
Usage Patterns
Automated Link Replacement
def replace_expired_links():
"""Automatically replace expired links for active accounts"""
expired_accounts = get_accounts_with_expired_links()
for account in expired_accounts:
try:
# Generate new link
new_link = generate_login_link(account['id'], {
'purpose': 'automated_renewal',
'expiry_hours': 168, # 7 days
'send_notification': True,
'notification_settings': {
'custom_message': 'Your previous login link has expired. Here is your new access link.'
}
})
print(f"Renewed link for account {account['id']}")
except Exception as e:
print(f"Failed to renew link for {account['id']}: {e}")
# Respect rate limits
time.sleep(1)
Support Ticket Integration
def generate_support_link(ticket_id, account_id, agent_name):
"""Generate login link for support ticket resolution"""
link_data = generate_login_link(account_id, {
'purpose': 'support_request',
'expiry_hours': 4, # Short expiry for security
'send_notification': True,
'notification_settings': {
'custom_subject': f'Support Access Link - Ticket #{ticket_id}',
'custom_message': f'Our support team has generated a secure access link to help resolve your inquiry. This link expires in 4 hours.',
'partner_branded': True
},
})
# Update support ticket with link info
update_ticket(ticket_id, {
'login_link_generated': True,
'link_expires_at': link_data['expires_at'],
'agent': agent_name
})
return link_data
Next Steps
Retrieve Account
Check account status and user information
Add Users
Add additional users who need login links
Account Activity
Monitor login link usage and account activity
Authentication
Learn about API security and authentication