Complete API reference for mobile app development
Last updated: 7/12/2026
Development: http://localhost:4000/api
Production: https://your-domain.com/api
Some endpoints require JWT authentication, while others are publicly accessible. For authenticated endpoints, include the token in the Authorization header:
Authorization: Bearer <your_jwt_token>
Public Endpoints (No Authentication Required):
Authenticated Endpoints:
All API responses follow this structure:
{
"message": "Success message",
"data": { /* response data */ },
"error": "Error message (if any)",
"timestamp": "2024-01-01T00:00:00.000Z"
}
Registration follows a secure 3-step process: phone → OTP → user details.
POST /otp/send/phone
Send OTP code to phone number for verification.
Request Body:
{
"phone": "+971501234567"
}
Response (200):
{
"message": "OTP sent successfully to phone number",
"expiresAt": "2024-01-01T10:05:00.000Z",
"phone": "+971****1234"
}
POST /otp/verify/phone
Verify OTP and receive temporary token for registration completion.
Request Body:
{
"phone": "+971501234567",
"code": "1234"
}
Response (200):
{
"message": "Phone verified successfully",
"tempToken": "temp_jwt_token_here",
"phone": "+971501234567",
"nextStep": "Complete registration with user details"
}
POST /otp/complete/registration
Complete registration with user details using temporary token.
Request Body:
{
"tempToken": "temp_jwt_token_here",
"email": "[email protected]",
"password": "securepassword123",
"firstName": "John",
"lastName": "Doe",
"userType": "tipper", // or "tippee"
"businessName": "Business Name", // required for tippees
"businessCategory": "restaurant" // required for tippees
}
Response (201):
{
"message": "Registration completed successfully",
"token": "jwt_token",
"user": {
"id": "user_id",
"email": "[email protected]",
"firstName": "John",
"lastName": "Doe",
"phone": "+971501234567",
"userType": "tipper",
"isPhoneVerified": true,
"qrCode": "QR123456", // only for tippees
"businessName": "Business Name", // only for tippees
"businessCategory": "restaurant" // only for tippees
}
}
POST /auth/mobile/login
Login for existing users on mobile apps.
Request Body:
{
"email": "[email protected]",
"password": "securepassword123",
"deviceInfo": {
"deviceId": "unique-device-id",
"platform": "ios", // or "android"
"appVersion": "1.0.0"
}
}
Response (200):
{
"message": "Login successful",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "refresh_jwt_token",
"user": {
"id": "user_id",
"email": "[email protected]",
"firstName": "John",
"lastName": "Doe",
"phone": "+971501234567",
"userType": "tipper",
"qrCode": "QR123456", // only for tippees
"businessName": "Business Name", // only for tippees
"businessCategory": "restaurant", // only for tippees
"requiresEmailUpdate": false,
"lastLogin": "2024-01-01T12:00:00.000Z"
}
}
POST /auth/mobile/login/otp/send
Send a one-time code to an existing account's phone number for passwordless login.
Request Body:
{
"phone": "+971501234567"
}
Response (200):
{
"message": "OTP sent successfully",
"expiresAt": "2024-01-01T10:05:00.000Z",
"phone": "+971****4567"
}
POST /auth/mobile/login/otp/verify
Verify the code and log in the user (recommended for newly claimed recipients with temporary emails/passwords).
Request Body:
{
"phone": "+971501234567",
"code": "1234",
"deviceInfo": {
"deviceId": "unique-device-id",
"platform": "android",
"appVersion": "1.0.0"
}
}
Response (200):
{
"message": "Login successful",
"token": "jwt_token",
"refreshToken": "refresh_jwt_token",
"user": {
"id": "user_id",
"email": "[email protected]",
"firstName": "New",
"lastName": "Tippee4567",
"phone": "+971501234567",
"userType": "tippee",
"qrCode": "QR123456",
"businessName": "",
"businessCategory": "",
"requiresEmailUpdate": true,
"lastLogin": "2024-01-01T12:00:00.000Z"
}
}
POST /auth/login/admin
Login endpoint for admin users.
Request Body:
{
"email": "[email protected]",
"password": "securepassword123"
}
Response (200):
{
"message": "Login successful",
"token": "jwt_token",
"user": {
"id": "user_id",
"email": "[email protected]",
"firstName": "Admin",
"lastName": "User",
"userType": "admin",
"lastLogin": "2024-01-01T12:00:00.000Z"
}
}
GET /users/profile
Auth Required: Yes
Response (200):
{
"message": "Profile retrieved successfully",
"user": {
"id": "user_id",
"email": "[email protected]",
"firstName": "John",
"lastName": "Doe",
"phone": "+971501234567",
"userType": "tippee",
"profileImage": "http://localhost:5000/uploads/profile-image.jpg",
"qrCode": "QR123456", // tippees only
"businessName": "John's Coffee Shop", // tippees only
"businessCategory": "restaurant", // tippees only
"paymentMethods": [
{
"type": "bank_account",
"details": {
"bankName": "Emirates NBD",
"accountNumber": "****1234",
"iban": "AE07****"
},
"isDefault": true
}
],
"totalReceived": 1250.50, // tippees only
"totalGiven": 89.25, // tippers only
"transactionCount": 15,
"isActive": true,
"isVerified": true,
"lastLogin": "2024-01-01T10:00:00.000Z"
}
}
PUT /users/profile
Auth Required: Yes
Request Body:
{
"firstName": "John",
"lastName": "Doe",
"phone": "+971501234567",
"businessName": "Updated Business Name", // tippees only
"businessCategory": "hotel" // tippees only
}
Response (200):
{
"message": "Profile updated successfully",
"user": { /* updated user object */ }
}
POST /users/profile/image
Auth Required: Yes
Content-Type: multipart/form-data
Request:
profileImage (image file)Response (200):
{
"message": "Profile image updated successfully",
"profileImage": "http://localhost:5000/uploads/profile-user_id-timestamp.jpg"
}
POST /users/payment-methods
Auth Required: Yes (Tippees only)
Request Body:
{
"type": "bank_account", // or "digital_wallet"
"details": {
"bankName": "Emirates NBD",
"accountNumber": "1234567890",
"iban": "AE070331234567890123456",
"accountHolderName": "John Doe"
},
"isDefault": true
}
Response (201):
{
"message": "Payment method added successfully",
"paymentMethod": {
"type": "bank_account",
"details": {
"bankName": "Emirates NBD",
"accountNumber": "****7890",
"iban": "AE07****",
"accountHolderName": "John Doe"
},
"isDefault": true
}
}
GET /users/transactions?page=1&limit=20&status=completed
Auth Required: Yes
Query Parameters:
page (optional): Page number (default: 1)limit (optional): Items per page (default: 20, max: 100)status (optional): Filter by status (pending, processing, completed, failed, refunded)startDate (optional): Filter from date (ISO string)endDate (optional): Filter to date (ISO string)Response (200):
{
"message": "Transactions retrieved successfully",
"transactions": [
{
"id": "transaction_id",
"transactionId": "TXN123456789",
"amount": 25.00,
"currency": "AED",
"paymentMethod": "apple_pay",
"status": "completed",
"message": "Great service!",
"rating": 5,
"fees": {
"serviceFee": 1.25,
"paymentFee": 0.85,
"totalFees": 2.10
},
"netAmount": 22.90,
"tipper": { // for tippees
"firstName": "Jane",
"lastName": "Smith",
"profileImage": "image_url"
},
"tippee": { // for tippers
"firstName": "John",
"lastName": "Doe",
"businessName": "John's Coffee Shop",
"profileImage": "image_url"
},
"createdAt": "2024-01-01T10:00:00.000Z",
"processedAt": "2024-01-01T10:01:00.000Z"
}
],
"pagination": {
"currentPage": 1,
"totalPages": 5,
"totalItems": 89,
"hasNext": true,
"hasPrev": false
}
}
POST /account/update-email
Auth Required: Yes
Allows authenticated users to replace temporary placeholder emails (for example @tiply.temp) with a real email address.
Request Body:
{
"email": "[email protected]"
}
Response (200):
{
"success": true,
"message": "Email updated successfully",
"user": {
"id": "user_id",
"email": "[email protected]",
"firstName": "John",
"lastName": "Doe",
"phone": "+971501234567",
"userType": "tippee",
"isPhoneVerified": true,
"qrCode": "QR123456",
"requiresEmailUpdate": false
}
}
Common Error Responses:
400 Validation Error (email is required, invalid format, or @tiply.temp domain)409 Conflict (Email is already in use)404 Not Found (User not found)GET /qr/generate
Auth Required: Yes (Tippees only)
Response (200):
{
"message": "QR code generated successfully",
"qrCode": "QR123456",
"qrCodeImage": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...", // Base64 image
"deepLink": "tiply://tip/QR123456",
"businessInfo": {
"name": "John's Coffee Shop",
"category": "restaurant",
"ownerName": "John Doe"
}
}
POST /qr/scan
Auth Required: No
Request Body:
{
"qrCode": "QR123456"
}
Response (200):
{
"message": "QR code scanned successfully",
"tippee": {
"id": "tippee_id",
"firstName": "John",
"lastName": "Doe",
"businessName": "John's Coffee Shop",
"businessCategory": "restaurant",
"profileImage": "image_url",
"qrCode": "QR123456"
},
"deepLink": "tiply://tip/QR123456"
}
GET /qr/validate/:qrCode
Auth Required: No
Response (200):
{
"message": "QR code is valid",
"valid": true,
"tippee": {
"businessName": "John's Coffee Shop",
"ownerName": "John Doe",
"category": "restaurant",
"isActive": true
}
}
POST /otp/generate/display-code
Auth Required: Yes (Tippees only)
Generate a 4-digit verification code to display on screen for payment authorization.
Request Body:
{}
Response (200):
{
"message": "Display code generated successfully",
"code": "1234",
"expiresAt": "2024-01-01T10:10:00.000Z",
"codeId": "code_id"
}
POST /otp/verify/display-code
Auth Required: No
Verify the display code shown by tippee to authorize payment. Can be called by anyone.
Request Body:
{
"code": "1234",
"tippeeId": "tippee_user_id"
}
Response (200):
{
"message": "Display code verified successfully",
"tippee": {
"id": "tippee_user_id",
"businessName": "John's Coffee Shop",
"ownerName": "John Doe"
},
"verificationToken": "verification_token_id"
}
GET /otp/display-code/status
Auth Required: Yes (Tippees only)
Check if there's an active display code and its status.
Response (200):
{
"hasActiveCode": true,
"code": "1234",
"expiresAt": "2024-01-01T10:10:00.000Z",
"expired": false,
"attemptsRemaining": 3
}
POST /otp/resend
Resend OTP for phone verification.
Request Body:
{
"phone": "+971501234567",
"type": "phone_verification"
}
Response (200):
{
"message": "OTP resent successfully",
"expiresAt": "2024-01-01T10:05:00.000Z",
"phone": "+971****1234"
}
The Tiply API supports anonymous payments, allowing users to send tips without creating an account. This provides a frictionless experience for spontaneous tipping.
| Feature | Anonymous Users | Authenticated Users |
|---|---|---|
| QR Code Scanning | ✅ | ✅ |
| Display Code Verification | ✅ | ✅ |
| Payment Processing | ✅ | ✅ |
| Transaction History | ❌ | ✅ |
| Profile Management | ❌ | ✅ |
| QR Code Generation | ❌ | ✅ (Tippees only) |
| Statistics Tracking | ❌ | ✅ |
Important: The mobile application must tokenize the Apple Pay/Google Pay payment data using Stripe's SDK before sending it to the backend.
Apple Pay Integration:
STPAPIClient.shared.createToken(with: payment) { (token, error) in
// Send token object to your backend
}
paymentData) in the paymentToken fieldGoogle Pay Integration:
stripe.createPaymentMethod(
PaymentMethodCreateParams.createFromGooglePay(googlePayResult)
)
paymentToken fieldPOST /payments/create
Auth Required: No
Create payment session (call after display code verification). Can be used by anyone - authenticated users (tippers or tippees) or anonymous users.
Request Body:
{
"tippeeId": "tippee_user_id",
"amount": 25.00,
"currency": "AED", // optional, defaults to AED
"paymentMethod": "apple_pay", // apple_pay, google_pay, card
"message": "Great service!", // optional
"location": { // optional
"latitude": 25.2048,
"longitude": 55.2708,
"address": "Dubai Mall, Dubai, UAE"
}
}
Response (201):
{
"message": "Payment session created successfully",
"transaction": {
"id": "transaction_id",
"transactionId": "TXN123456789",
"amount": 25.00,
"currency": "AED",
"paymentMethod": "apple_pay",
"status": "processing",
"fees": {
"serviceFee": 1.25,
"paymentFee": 0.85,
"totalFees": 2.10
},
"netAmount": 22.90
},
"paymentSession": {
"sessionId": "pi_1234567890", // Stripe Payment Intent ID
"clientSecret": "pi_1234567890_secret_xyz", // For client-side confirmation
"expiresAt": null // Stripe Payment Intents don't expire by default
},
"tippee": {
"id": "tippee_user_id",
"businessName": "John's Coffee Shop",
"ownerName": "John Doe"
}
}
Important - Payment Flow with Stripe Connect:
Error Responses:
404 - Tippee Not Found: Invalid tippee ID or account inactive400 - Tippee Setup Incomplete: Tippee has not completed Stripe Connect onboarding400 - Payment Session Failed: Error creating payment session with StripePOST /payments/apple-pay
Auth Required: No
Request Body:
{
"transactionId": "TXN123456789",
"paymentToken": {
"id": "tok_1234567890",
"object": "token",
"card": {
"id": "card_1234567890",
"object": "card",
"brand": "Visa",
"country": "US",
"exp_month": 12,
"exp_year": 2025,
"last4": "4242",
"funding": "credit"
},
"type": "card",
"created": 1234567890,
"livemode": false,
"used": false
}
}
Note: The paymentToken is a Stripe token created by the mobile app using Stripe's iOS SDK. The mobile app obtains the PKPaymentToken from Apple Pay, then uses Stripe SDK to convert it to a Stripe token before sending it to the backend.
Response (200):
{
"message": "Apple Pay payment processed successfully",
"transaction": {
"id": "transaction_id",
"transactionId": "TXN123456789",
"status": "completed",
"amount": 25.00,
"netAmount": 22.90
}
}
POST /payments/google-pay
Auth Required: No
Request Body:
{
"transactionId": "TXN123456789",
"paymentToken": {
"id": "tok_1234567890",
"object": "token",
"card": {
"id": "card_1234567890",
"object": "card",
"brand": "MasterCard",
"country": "US",
"exp_month": 12,
"exp_year": 2025,
"last4": "8210",
"funding": "debit"
},
"type": "card",
"created": 1234567890,
"livemode": false,
"used": false
}
}
Note: The paymentToken is a Stripe token created by the mobile app using Stripe's Android SDK. The mobile app obtains the payment data from Google Pay, then uses Stripe SDK to convert it to a Stripe token before sending it to the backend.
Response (200):
{
"message": "Google Pay payment processed successfully",
"transaction": {
"id": "transaction_id",
"transactionId": "TXN123456789",
"status": "completed",
"amount": 25.00,
"netAmount": 22.90
}
}
GET /payments/status/:transactionId
Auth Required: No
Response (200):
{
"message": "Payment status retrieved successfully",
"payment": {
"id": "pi_1234567890",
"status": "succeeded", // requires_payment_method, requires_confirmation, requires_action, processing, requires_capture, canceled, succeeded
"amount": 27.10, // including fees
"currency": "aed",
"transactionId": "TXN123456789",
"createdAt": "2024-01-01T10:00:00.000Z"
}
}
GET /payments/methods
Auth Required: No
Response (200):
{
"message": "Payment methods retrieved successfully",
"paymentMethods": [
"card",
"apple_pay",
"google_pay",
"link"
]
}
POST /payments/:transactionId/feedback
Auth Required: No
Request Body:
{
"rating": 5, // 1-5
"feedback": "Excellent service and very friendly staff!" // optional
}
Response (200):
{
"message": "Feedback added successfully",
"transaction": {
"id": "transaction_id",
"rating": 5,
"feedback": "Excellent service and very friendly staff!"
}
}
POST /payments/refund
Auth Required: Yes
Request Body:
{
"transactionId": "TXN123456789",
"reason": "Service not satisfactory", // optional
"amount": 25.00 // optional, full refund if not specified
}
Response (200):
{
"message": "Refund processed successfully",
"refund": {
"id": "re_1234567890",
"amount": 25.00,
"status": "succeeded",
"reason": "Service not satisfactory"
}
}
These endpoints support the anonymous-recipient tip flow:
Claim Lifecycle Statuses:
PENDINGPAID_UNCLAIMEDCOMPLETEDFAILEDREFUNDEDEXPIREDPOST /claim/create
Auth Required: Yes
Request Body:
{
"amount": 25.0,
"message": "Thanks for the great service!",
"rating": 5,
"paymentMethod": "card",
"tipperCoversProcessingFee": false
}
Response (201):
{
"transaction": {
"claimId": "CLM_MBE2X1_9ZKQW",
"status": "PENDING",
"amount": 25,
"chargeAmount": 26.15,
"platformFee": 1.25,
"processingFee": 0.9,
"netAmount": 23.75,
"tipperCoversProcessingFee": false,
"currency": "AED",
"expiresAt": "2026-06-14T10:00:00.000Z"
},
"paymentSession": {
"clientSecret": "pi_abc_secret_xyz",
"paymentIntentId": "pi_abc"
},
"claimToken": "f9d8c4f9-60f8-4a75-9f84-3fd84f17b7f1"
}
POST /claim/apple-pay
Auth Required: Optional
Request Body:
{
"transactionId": "CLM_MBE2X1_9ZKQW",
"paymentToken": {
"id": "tok_1234567890",
"object": "token",
"type": "card"
}
}
Response (200):
{
"message": "Apple Pay processed successfully",
"transaction": {
"id": "claim_object_id",
"transactionId": "CLM_MBE2X1_9ZKQW",
"status": "PAID_UNCLAIMED",
"amount": 25,
"netAmount": 23.75
}
}
POST /claim/google-pay
Auth Required: Optional
Request Body:
{
"transactionId": "CLM_MBE2X1_9ZKQW",
"paymentToken": {
"id": "tok_1234567890",
"object": "token",
"type": "card"
}
}
Response (200):
{
"message": "Google Pay processed successfully",
"transaction": {
"id": "claim_object_id",
"transactionId": "CLM_MBE2X1_9ZKQW",
"status": "PAID_UNCLAIMED",
"amount": 25,
"netAmount": 23.75
}
}
POST /claim/:claimId/confirm
Auth Required: Optional
Checks the Stripe PaymentIntent status and marks the claim as paid when status is succeeded.
Response (200):
{
"success": true,
"status": "PAID_UNCLAIMED",
"message": "Payment confirmed"
}
GET /claim/:claimToken
Auth Required: No
Used by the web claim page after scanning/opening the QR link.
Response (200):
{
"claimId": "CLM_MBE2X1_9ZKQW",
"claimToken": "f9d8c4f9-60f8-4a75-9f84-3fd84f17b7f1",
"amount": 25,
"currency": "AED",
"message": "Thanks for the great service!",
"status": "PAID_UNCLAIMED",
"expiresAt": "2026-06-14T10:00:00.000Z",
"claimedAt": null,
"isClaimable": true
}
Response (410 - Expired):
{
"claimId": "CLM_MBE2X1_9ZKQW",
"status": "EXPIRED",
"message": "This tip has expired and was automatically refunded."
}
POST /claim/:claimToken/send-otp
Auth Required: No
Request Body:
{
"phone": "+971501234567"
}
Response (200):
{
"message": "OTP sent successfully to phone number",
"expiresAt": "2026-06-07T10:05:00.000Z",
"phone": "+971****4567"
}
POST /claim/:claimToken/verify-otp
Auth Required: No
Request Body:
{
"phone": "+971501234567",
"code": "1234"
}
Response (200):
{
"message": "Tip claimed successfully",
"token": "jwt_token",
"refreshToken": "refresh_jwt_token",
"newlyCreated": true,
"user": {
"id": "tippee_user_id",
"email": "[email protected]",
"firstName": "New",
"lastName": "Tippee4567",
"phone": "+971501234567",
"userType": "tippee",
"requiresEmailUpdate": true,
"qrCode": "QR123456",
"publicSlug": "new-tippee4567",
"businessName": "",
"profileImage": ""
},
"claim": {
"claimId": "CLM_MBE2X1_9ZKQW",
"status": "COMPLETED",
"amount": 25,
"netAmount": 23.75,
"currency": "AED",
"claimedAt": "2026-06-07T10:04:00.000Z"
}
}
GET /claim/:claimId/status
Auth Required: Optional
Response (200):
{
"claimId": "CLM_MBE2X1_9ZKQW",
"status": "COMPLETED",
"amount": 25,
"currency": "AED",
"message": "Thanks for the great service!",
"completedAt": "2026-06-07T10:01:00.000Z",
"expiresAt": "2026-06-14T10:00:00.000Z",
"tippee": {
"id": "tippee_user_id",
"name": "New Tippee4567",
"phone": "+971501234567",
"publicSlug": "new-tippee4567",
"profileImage": ""
}
}
GET /claim/my/created?page=1&limit=20
Auth Required: Yes
Response (200):
{
"claims": [
{
"claimId": "CLM_MBE2X1_9ZKQW",
"amount": 25,
"chargeAmount": 26.15,
"currency": "AED",
"status": "PAID_UNCLAIMED",
"message": "Thanks for the great service!",
"claimedAt": null,
"expiresAt": "2026-06-14T10:00:00.000Z",
"createdAt": "2026-06-07T10:00:00.000Z",
"tippee": null
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 1,
"pages": 1
}
}
GET /users/stats
Auth Required: Yes (Tippees only)
Response (200):
{
"message": "Stats retrieved successfully",
"stats": {
"totalReceived": 2450.75,
"totalTransactions": 98,
"averageTip": 25.01,
"thisMonth": {
"received": 450.25,
"transactions": 18,
"growth": 12.5 // percentage
},
"topPaymentMethods": [
{ "method": "apple_pay", "count": 45, "percentage": 45.9 },
{ "method": "google_pay", "count": 32, "percentage": 32.7 },
{ "method": "card", "count": 21, "percentage": 21.4 }
],
"recentTransactions": [
// Last 5 transactions
]
}
}
GET /users/search?q=coffee&category=restaurant&location=dubai
Auth Required: Optional
Query Parameters:
q (optional): Search query (business name, owner name)category (optional): Business category filterlocation (optional): Location filterpage (optional): Page numberlimit (optional): Items per pageResponse (200):
{
"message": "Search results retrieved successfully",
"tippees": [
{
"id": "tippee_id",
"firstName": "John",
"lastName": "Doe",
"businessName": "John's Coffee Shop",
"businessCategory": "restaurant",
"profileImage": "image_url",
"qrCode": "QR123456",
"rating": 4.8,
"totalTransactions": 156
}
],
"pagination": {
"currentPage": 1,
"totalPages": 3,
"totalItems": 25
}
}
All endpoints in this section require an authenticated admin token.
GET /admin/transactions?page=1&limit=20&status=COMPLETED&sortBy=createdAt&sortOrder=desc
Auth Required: Yes (Admin)
Returns a merged transaction list from both direct tips (Tip) and claimable tips (ClaimableTip).
Query Parameters:
page (optional): Page number (default: 1)limit (optional): Page size (default: 20)status (optional): Transaction status filterpaymentMethod (optional): card, apple_pay, google_pay, etc.dateFrom, dateTo (optional): Created date range (ISO date)minAmount, maxAmount (optional): Amount rangesortBy (optional): Any transaction field (default: createdAt)sortOrder (optional): asc or desc (default: desc)Response (200):
{
"message": "Transactions retrieved successfully",
"transactions": [
{
"_id": "object_id",
"transactionId": "CLM_MBE2X1_9ZKQW",
"transactionType": "claimable",
"tipper": {
"_id": "tipper_id",
"firstName": "John",
"lastName": "Doe",
"email": "[email protected]"
},
"tipperName": "John Doe",
"tipperEmail": "[email protected]",
"tippee": null,
"amount": 25,
"platformFee": 1.25,
"processingFee": 0.9,
"chargeAmount": 26.15,
"tipperCoversProcessingFee": false,
"netAmount": 23.75,
"currency": "AED",
"paymentMethod": "apple_pay",
"status": "PAID_UNCLAIMED",
"claimStatus": "PAID_UNCLAIMED",
"message": "Thanks for the great service!",
"rating": 5,
"createdAt": "2026-06-07T10:00:00.000Z",
"updatedAt": "2026-06-07T10:01:00.000Z",
"completedAt": "2026-06-07T10:01:00.000Z",
"claimedAt": null,
"expiresAt": "2026-06-14T10:00:00.000Z",
"refundedAt": null,
"refundReason": null
}
],
"pagination": {
"currentPage": 1,
"totalPages": 1,
"totalTransactions": 1,
"hasNext": false,
"hasPrev": false
}
}
GET /admin/transactions/:transactionId
Auth Required: Yes (Admin)
Supports both direct tip IDs and claim IDs (CLM_...).
Response (200):
{
"message": "Transaction details retrieved successfully",
"transaction": {
"_id": "object_id",
"transactionId": "CLM_MBE2X1_9ZKQW",
"transactionType": "claimable",
"tipper": {
"_id": "tipper_id",
"firstName": "John",
"lastName": "Doe",
"email": "[email protected]",
"phone": "+971501234567",
"userType": "tipper"
},
"tipperName": "John Doe",
"tipperEmail": "[email protected]",
"tippee": null,
"amount": 25,
"platformFee": 1.25,
"processingFee": 0.9,
"chargeAmount": 26.15,
"tipperCoversProcessingFee": false,
"netAmount": 23.75,
"currency": "AED",
"paymentMethod": "apple_pay",
"stripePaymentIntentId": "pi_abc",
"status": "PAID_UNCLAIMED",
"claimStatus": "PAID_UNCLAIMED",
"claimedAt": null,
"expiresAt": "2026-06-14T10:00:00.000Z",
"message": "Thanks for the great service!",
"rating": 5,
"feedback": null,
"createdAt": "2026-06-07T10:00:00.000Z",
"updatedAt": "2026-06-07T10:01:00.000Z",
"completedAt": "2026-06-07T10:01:00.000Z",
"refundedAt": null,
"refundReason": null
}
}
Response (404):
{
"error": "Transaction Not Found",
"message": "Transaction not found"
}
400 - Bad Request (validation errors)401 - Unauthorized (missing or invalid token)403 - Forbidden (insufficient permissions)404 - Not Found409 - Conflict (duplicate data)422 - Unprocessable Entity (business logic errors)429 - Too Many Requests (rate limiting)500 - Internal Server Error{
"error": "Validation Error",
"message": "Email is required",
"details": {
"field": "email",
"code": "REQUIRED"
},
"timestamp": "2024-01-01T10:00:00.000Z"
}
POST /payments/webhook
This endpoint receives payment status updates from Stripe. Your mobile app should listen for real-time updates via WebSocket or implement polling for payment status.
For users who want to create accounts:
/auth/mobile/login for existing users or the 3-step registration process for new usersFor anonymous users:
For Authenticated Users (Tippers or Tippees):
/otp/generate/display-code/otp/verify/display-code/payments/create/payments/apple-pay or /payments/google-payFor Anonymous Users:
/otp/generate/display-code/otp/verify/display-code (no auth required)/payments/create (no auth required)/payments/apple-pay or /payments/google-pay (no auth required)Note: Both tippers and tippees can make payments to other tippees. Anonymous users can also make payments without creating an account.
If Twilio API keys are not configured, the system uses "1234" as the registration OTP code for testing.
For production, configure Twilio environment variables:
TWILIO_ACCOUNT_SID=your_account_sid
TWILIO_AUTH_TOKEN=your_auth_token
TWILIO_FROM_NUMBER=your_twilio_phone_number
GET /withdrawals/balance
Auth Required: Yes (Tippees only)
Get tippee's available balance for withdrawal.
Response (200):
{
"message": "Balance retrieved successfully",
"balance": {
"available": 150.50,
"pending": 25.00,
"total": 1250.75,
"currency": "AED"
}
}
POST /withdrawals/setup-connect
Auth Required: Yes (Tippees only)
Setup Stripe Connect account for receiving payouts. This must be completed before adding payout methods or withdrawing funds.
Response (200):
{
"message": "Stripe Connect setup initiated",
"onboardingUrl": "https://connect.stripe.com/setup/...",
"expiresAt": "2024-01-01T12:00:00.000Z"
}
Mobile App Implementation:
onboardingUrl in an external browser (not WebView - Stripe requirement){SERVER_URL}/api/withdrawals/stripe/connect/return{MOBILE_APP_URL}stripe/connect/return/withdrawals/connect-status to verify onboarding completionDeep Link Configuration:
iOS (Info.plist):
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>tiply</string>
</array>
</dict>
</array>
Android (AndroidManifest.xml):
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="tiply" />
</intent-filter>
Server Configuration:
# Set your server's public URL
SERVER_URL=https://api.tiply.app
# Set mobile app deep link scheme
MOBILE_APP_URL=tiply://
GET /withdrawals/connect-status
Auth Required: Yes (Tippees only)
Check if Stripe Connect onboarding is complete. Call this after user returns from Stripe onboarding flow.
Response (200):
{
"message": "Stripe Connect status retrieved successfully",
"onboarded": true,
"requiresOnboarding": false,
"details": {
"detailsSubmitted": true,
"chargesEnabled": true,
"payoutsEnabled": true,
"requirementsCurrentlyDue": [],
"requirementsPastDue": []
}
}
Response Fields:
onboarded: true if onboarding complete and account ready for payoutsrequiresOnboarding: true if user needs to complete/re-complete onboardingrequirementsCurrentlyDue: Array of requirements that need to be fulfilled immediatelyrequirementsPastDue: Array of requirements that are overdueImportant Behavior:
onboarded changes from false to true, the server automatically fetches and saves all payout methods (bank accounts/cards) that the user added during Stripe onboarding.onboarded: true, you can immediately call GET /withdrawals/payout-methods to see the synced accounts.POST /withdrawals/payout-methods after onboarding - they're already there!Note: If onboarded: false, call /withdrawals/setup-connect again to get a fresh onboarding URL.
POST /withdrawals/payout-methods
Auth Required: Yes (Tippees only)
Add a bank account or debit card for receiving withdrawals.
Prerequisites:
onboarded: true from /connect-status)Request Body:
{
"accountToken": "btok_1234567890" // Stripe bank account or card token
}
How to get accountToken:
The accountToken must be created client-side in the mobile app using Stripe SDK. Never send raw bank account details to the server.
iOS Example (Swift):
import Stripe
let bankAccountParams = STPBankAccountParams()
bankAccountParams.accountNumber = "000123456789"
bankAccountParams.routingNumber = "110000000"
bankAccountParams.accountHolderName = "John Doe"
bankAccountParams.accountHolderType = .individual
bankAccountParams.country = "AE"
bankAccountParams.currency = "aed"
STPAPIClient.shared.createToken(withBankAccount: bankAccountParams) { token, error in
if let token = token {
// Send token.tokenId to backend
addPayoutMethod(accountToken: token.tokenId)
}
}
Android Example (Kotlin):
import com.stripe.android.Stripe
import com.stripe.android.model.BankAccount
val bankAccount = BankAccount(
accountNumber = "000123456789",
routingNumber = "110000000",
accountHolderName = "John Doe",
accountHolderType = BankAccount.Type.Individual,
country = "AE",
currency = "aed"
)
stripe.createBankAccountToken(
bankAccount = bankAccount,
callback = object : ApiResultCallback<Token> {
override fun onSuccess(result: Token) {
// Send result.id to backend
addPayoutMethod(accountToken = result.id)
}
}
)
Required Information from User:
Response (200):
{
"message": "Payout method added successfully",
"payoutMethod": {
"id": "ba_1234567890",
"type": "bank_account",
"last4": "1234",
"bankName": "Example Bank",
"isDefault": true
}
}
Error Responses:
400 - Validation Error: Account token is required400 - No Connect Account: Stripe Connect onboarding not complete400 - External Account Failed: Invalid bank account details403 - Forbidden: User is not a tippeeGET /withdrawals/payout-methods
Auth Required: Yes (Tippees only)
Get list of saved payout methods.
Response (200):
{
"message": "Payout methods retrieved successfully",
"payoutMethods": [
{
"id": "ba_1234567890",
"type": "bank_account",
"last4": "1234",
"bankName": "Example Bank",
"country": "AE",
"currency": "aed",
"isDefault": true,
"addedAt": "2024-01-01T00:00:00.000Z"
},
{
"id": "card_0987654321",
"type": "debit_card",
"last4": "5678",
"bankName": "Another Bank",
"country": "AE",
"currency": "aed",
"isDefault": false,
"addedAt": "2024-01-02T00:00:00.000Z"
}
]
}
PUT /withdrawals/payout-methods/:methodId/default
Auth Required: Yes (Tippees only)
Set a payout method as default for withdrawals.
Response (200):
{
"message": "Default payout method updated successfully"
}
DELETE /withdrawals/payout-methods/:methodId
Auth Required: Yes (Tippees only)
Delete a saved payout method.
Response (200):
{
"message": "Payout method deleted successfully"
}
Note: Cannot delete the only payout method. If deleting the default method, the first remaining method will be set as default.
POST /withdrawals/withdraw
Auth Required: Yes (Tippees only)
Initiate a withdrawal to a saved payout method.
Request Body:
{
"amount": 100.00,
"currency": "AED", // optional, defaults to AED
"destinationId": "ba_1234567890" // optional, uses default if not provided
}
Response (200):
{
"message": "Withdrawal initiated successfully",
"withdrawal": {
"id": "withdrawal_db_id",
"withdrawalId": "WD1234567890ABCD",
"amount": 100.00,
"currency": "AED",
"status": "processing",
"expectedArrivalDate": "2024-01-05T00:00:00.000Z",
"fees": {
"stripeFee": 0,
"totalFees": 0
},
"netAmount": 100.00
}
}
Error Responses:
400 - Insufficient balance, below minimum amount, or no payout methods403 - User is not a tippee500 - Server errorMinimum Withdrawal: 10 AED
GET /withdrawals/history?limit=20&skip=0&status=completed
Auth Required: Yes (Tippees only)
Get withdrawal history with pagination.
Query Parameters:
limit (optional): Number of records per page (default: 20)skip (optional): Number of records to skip (default: 0)status (optional): Filter by status (pending, processing, completed, failed, cancelled)Response (200):
{
"message": "Withdrawal history retrieved successfully",
"withdrawals": [
{
"id": "withdrawal_id",
"withdrawalId": "WD1234567890ABCD",
"amount": 100.00,
"currency": "AED",
"status": "completed",
"destinationType": "bank_account",
"expectedArrivalDate": "2024-01-05T00:00:00.000Z",
"completedAt": "2024-01-05T10:30:00.000Z",
"fees": {
"stripeFee": 0,
"totalFees": 0
},
"netAmount": 100.00,
"createdAt": "2024-01-03T12:00:00.000Z"
}
],
"pagination": {
"total": 25,
"limit": 20,
"skip": 0,
"hasMore": true
}
}
GET /withdrawals/:withdrawalId
Auth Required: Yes (Tippees only)
Get status of a specific withdrawal.
Response (200):
{
"message": "Withdrawal status retrieved successfully",
"withdrawal": {
"id": "withdrawal_id",
"withdrawalId": "WD1234567890ABCD",
"amount": 100.00,
"currency": "AED",
"status": "processing",
"destinationType": "bank_account",
"expectedArrivalDate": "2024-01-05T00:00:00.000Z",
"completedAt": null,
"failureCode": null,
"failureMessage": null,
"fees": {
"stripeFee": 0,
"totalFees": 0
},
"netAmount": 100.00,
"createdAt": "2024-01-03T12:00:00.000Z"
}
}
Withdrawal Statuses:
pending: Withdrawal created but not yet sent to Stripeprocessing: Payout is being processed by Stripecompleted: Funds have been successfully transferredfailed: Payout failed (funds returned to available balance)cancelled: Withdrawal was cancelled by userPOST /withdrawals/:withdrawalId/cancel
Auth Required: Yes (Tippees only)
Cancel a pending withdrawal. Only pending withdrawals can be cancelled.
Response (200):
{
"message": "Withdrawal cancelled successfully"
}
Error Response:
400 - Cannot cancel (withdrawal already processing or completed)GET /withdrawals/stripe/connect/refresh
Auth Required: No (Public endpoint)
This endpoint is automatically called by Stripe when the onboarding link expires. It displays an HTML page that redirects the user back to the mobile app with a refresh message.
Response: HTML page with automatic redirect to {MOBILE_APP_URL}stripe/connect/refresh
Note: This is a system endpoint - mobile apps don't call this directly. Stripe uses it during the onboarding flow.
GET /withdrawals/stripe/connect/return
Auth Required: No (Public endpoint)
This endpoint is automatically called by Stripe when the user completes or exits the onboarding flow. It displays an HTML page that redirects the user back to the mobile app.
Response: HTML page with automatic redirect to {MOBILE_APP_URL}stripe/connect/return
Mobile App Handling:
When the app receives the deep link tiply://stripe/connect/return, it should:
GET /withdrawals/connect-status to check onboarding statusonboarded statusNote: This is a system endpoint - mobile apps don't call this directly. Stripe uses it during the onboarding flow.
POST /withdrawals/setup-connect
stripeConnectAccountIdonboardingUrlGET /withdrawals/connect-status
onboarded: truefalse, user needs to complete additional requirementsrequirementsCurrentlyDue exists, show user what's neededstripeConnectOnboarded = true when verifiedGET /withdrawals/payout-methods
POST /withdrawals/payout-methodsOptional: Add Additional Payout Method If user wants to add another bank account/card after onboarding:
On Mobile App (Client-Side):
// iOS
STPAPIClient.shared.createToken(withBankAccount: params) { token, error in
// Send token.tokenId to backend
}
On Backend:
POST /withdrawals/payout-methods
Body: { "accountToken": "btok_..." }
GET /withdrawals/balance
POST /withdrawals/withdraw
Body: { "amount": 100.00, "currency": "AED", "destinationId": "ba_..." }
GET /withdrawals/:withdrawalId
Generated automatically from API_DOCUMENTATION.md
For technical support, contact the development team