🚀 Tiply API Documentation

Complete API reference for mobile app development

Last updated: 7/12/2026

📋 Table of Contents

Tiply Mobile App API Documentation

Base URL

Development: http://localhost:4000/api
Production: https://your-domain.com/api

Authentication

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:

Response Format

All API responses follow this structure:

{
  "message": "Success message",
  "data": { /* response data */ },
  "error": "Error message (if any)",
  "timestamp": "2024-01-01T00:00:00.000Z"
}

🔐 Authentication Endpoints

3-Step Registration Flow

Registration follows a secure 3-step process: phone → OTP → user details.

Step 1: Send OTP to Phone

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"
}

Step 2: Verify Phone OTP

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"
}

Step 3: Complete Registration

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
  }
}

1. Mobile Login (Existing Users)

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"
  }
}

2. Mobile OTP Login - Send Code

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"
}

3. Mobile OTP Login - Verify Code

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"
  }
}

4. Admin Login

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"
  }
}

👤 User Profile Endpoints

1. Get User Profile

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"
  }
}

2. Update Profile

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 */ }
}

3. Upload Profile Image

POST /users/profile/image Auth Required: Yes Content-Type: multipart/form-data

Request:

Response (200):

{
  "message": "Profile image updated successfully",
  "profileImage": "http://localhost:5000/uploads/profile-user_id-timestamp.jpg"
}

4. Add Payment Method

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
  }
}

5. Get Transaction History

GET /users/transactions?page=1&limit=20&status=completed Auth Required: Yes

Query Parameters:

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
  }
}

6. Update Account Email

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:


🔄 QR Code Endpoints

1. Generate QR Code (Tippees)

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"
  }
}

2. Scan QR Code (Anyone)

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"
}

3. Validate QR Code

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
  }
}

📱 Display Code Endpoints

1. Generate Display Code (Tippee)

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"
}

2. Verify Display Code (Anyone)

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"
}

3. Check Display Code Status (Tippee)

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
}

3. Resend Phone Verification OTP

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"
}

🔓 Anonymous Payments

The Tiply API supports anonymous payments, allowing users to send tips without creating an account. This provides a frictionless experience for spontaneous tipping.

Key Features:

Security Considerations:

Anonymous vs Authenticated Users:

Feature Anonymous Users Authenticated Users
QR Code Scanning
Display Code Verification
Payment Processing
Transaction History
Profile Management
QR Code Generation ✅ (Tippees only)
Statistics Tracking

💳 Payment Endpoints

Mobile SDK Integration

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:

  1. Use Apple's PassKit framework to obtain the PKPaymentToken from Apple Pay
  2. Use Stripe iOS SDK to convert the PKPaymentToken to a Stripe token:
    STPAPIClient.shared.createToken(with: payment) { (token, error) in
        // Send token object to your backend
    }
    
  3. Send the entire Stripe token object (including paymentData) in the paymentToken field

Google Pay Integration:

  1. Use Google Pay API to obtain the PaymentData
  2. Use Stripe Android SDK to convert the Google Pay token to a Stripe token:
    stripe.createPaymentMethod(
        PaymentMethodCreateParams.createFromGooglePay(googlePayResult)
    )
    
  3. Send the token object in the paymentToken field

1. Create Payment Session

POST /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:

3. Process Apple Pay

POST /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
  }
}

4. Process Google Pay

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
  }
}

5. Get Payment Status

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"
  }
}

6. Get Supported Payment Methods

GET /payments/methods Auth Required: No

Response (200):

{
  "message": "Payment methods retrieved successfully",
  "paymentMethods": [
    "card",
    "apple_pay",
    "google_pay",
    "link"
  ]
}

7. Add Transaction Feedback

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!"
  }
}

7. Request Refund

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"
  }
}

🎁 Claimable Tip Endpoints

These endpoints support the anonymous-recipient tip flow:

  1. Authenticated tipper creates a claimable tip.
  2. Tipper pays (Apple Pay / Google Pay / card flow).
  3. Recipient opens claim link, verifies phone via OTP, and claims funds.

Claim Lifecycle Statuses:

1. Create Claimable Tip

POST /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"
}

2. Process Claimable Apple Pay

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
  }
}

3. Process Claimable Google Pay

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
  }
}

4. Confirm Claim Payment

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."
}

6. Send Claim OTP

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"
}

7. Verify Claim OTP and Complete Claim

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"
  }
}

8. Get Claim Status

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": ""
  }
}

9. List My Created Claims

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
  }
}

📊 Analytics Endpoints (Tippees)

1. Get Dashboard Stats

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
    ]
  }
}

🔍 Search Endpoints

1. Search Tippees

GET /users/search?q=coffee&category=restaurant&location=dubai Auth Required: Optional

Query Parameters:

Response (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
  }
}

🛡️ Admin Transaction Endpoints

All endpoints in this section require an authenticated admin token.

1. List Transactions (Direct + Claimable)

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:

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
  }
}

2. Get Transaction Details (Direct or Claimable)

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"
}

⚠️ Error Responses

Common Error Codes:

Error Response Format:

{
  "error": "Validation Error",
  "message": "Email is required",
  "details": {
    "field": "email",
    "code": "REQUIRED"
  },
  "timestamp": "2024-01-01T10:00:00.000Z"
}

🔄 Webhooks (For Payment Updates)

Stripe Webhook Endpoint

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.


📱 Mobile App Integration Notes

1. **Authentication Flow**

For users who want to create accounts:

  1. Use /auth/mobile/login for existing users or the 3-step registration process for new users
  2. Store JWT token securely (Keychain/Keystore)
  3. Include token in authenticated requests

For anonymous users:

2. **Payment Flow**

For Authenticated Users (Tippers or Tippees):

  1. User scans QR code to get tippee information
  2. Tippee generates display code using /otp/generate/display-code
  3. User verifies display code using /otp/verify/display-code
  4. User creates payment session using /payments/create
  5. User processes payment using /payments/apple-pay or /payments/google-pay

For Anonymous Users:

  1. Anyone scans QR code to get tippee information (no auth required)
  2. Tippee generates display code using /otp/generate/display-code
  3. Anyone verifies display code using /otp/verify/display-code (no auth required)
  4. Anyone creates payment session using /payments/create (no auth required)
  5. Anyone processes payment using /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.

3. **QR Code Integration**


🚀 Getting Started

  1. Update base URL for production
  2. Implement JWT token management
  3. Configure Stripe SDK with your publishable key
  4. Set up Twilio SMS service for OTP functionality
  5. Implement error handling and loading states

Development Mode

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

💰 Withdrawal Endpoints (Tippees Only)

1. Get Balance

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"
  }
}

2. Setup Stripe Connect Account

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:

  1. Open the onboardingUrl in an external browser (not WebView - Stripe requirement)
  2. User completes Stripe's identity verification and business details
  3. Stripe redirects to server endpoint: {SERVER_URL}/api/withdrawals/stripe/connect/return
  4. Server displays HTML page that automatically redirects to mobile app via deep link: {MOBILE_APP_URL}stripe/connect/return
  5. Mobile app handles the deep link (configure in iOS/Android app settings)
  6. Call /withdrawals/connect-status to verify onboarding completion

Deep 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://

3. Check Stripe Connect Status

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:

Important Behavior:

Note: If onboarded: false, call /withdrawals/setup-connect again to get a fresh onboarding URL.

4. Add Payout Method

POST /withdrawals/payout-methods Auth Required: Yes (Tippees only)

Add a bank account or debit card for receiving withdrawals.

Prerequisites:

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:

5. Get Payout Methods

GET /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"
    }
  ]
}

6. Set Default Payout Method

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"
}

7. Delete Payout Method

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.

8. Initiate Withdrawal

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:

Minimum Withdrawal: 10 AED

9. Get Withdrawal History

GET /withdrawals/history?limit=20&skip=0&status=completed Auth Required: Yes (Tippees only)

Get withdrawal history with pagination.

Query Parameters:

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
  }
}

10. Get Withdrawal Status

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:

11. Cancel Withdrawal

POST /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:

12. Stripe Connect Refresh Redirect

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.

13. Stripe Connect Return Redirect

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:

  1. Navigate user to the withdrawals/settings screen
  2. Call GET /withdrawals/connect-status to check onboarding status
  3. Display appropriate message based on onboarded status

Note: This is a system endpoint - mobile apps don't call this directly. Stripe uses it during the onboarding flow.


🔄 Withdrawal Flow

Complete Withdrawal Process:

1. **Setup Stripe Connect** (One-time - Required before first withdrawal)

POST /withdrawals/setup-connect

2. **Verify Onboarding Complete** (After returning from Stripe)

GET /withdrawals/connect-status

3. **View Payout Methods** (Automatically synced from Stripe)

GET /withdrawals/payout-methods

Optional: Add Additional Payout Method If user wants to add another bank account/card after onboarding:

On Mobile App (Client-Side):

On Backend:

POST /withdrawals/payout-methods
Body: { "accountToken": "btok_..." }

4. **Check Balance**

GET /withdrawals/balance

5. **Initiate Withdrawal**

POST /withdrawals/withdraw
Body: { "amount": 100.00, "currency": "AED", "destinationId": "ba_..." }

6. **Track Status**

GET /withdrawals/:withdrawalId

Balance Types:

Important Notes:


Generated automatically from API_DOCUMENTATION.md

For technical support, contact the development team