Skip to content

Visa API Specification

Overview

The Visa API provides endpoints for managing visa applications, applicants, documents, deliveries, invoices, and requirements.

Base Path: /visa

Content Types: - Request: application/json - Response: application/json

Response Format

All endpoints return a TlinqApiResponse object:

{
  "apiStatus": {
    "errorCode": "OK",
    "errorMessage": "Success"
  },
  "apiData": { ... }
}

Date Format: All dates are returned in ISO 8601 format (yyyy-MM-dd'T'HH:mm:ss)


Application Endpoints

POST /visa/readapplication

Reads a visa application by ID.

Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | session | string | No | User session token | | applicationId | integer | Yes | Application ID |

Request Example:

{
  "session": "user-session-token",
  "applicationId": 5001
}

Response Structure:

{
  "apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
  "apiData": {
    "applicationId": 5001,
    "applicationNumber": "VISA-2025-005001",
    "customerId": 1001,
    "customerName": "John Smith",
    "destinationCountry": "GB",
    "destinationCountryName": "United Kingdom",
    "visaType": "TOURIST",
    "travelDate": "2025-08-15",
    "returnDate": "2025-08-30",
    "status": "IN_PROGRESS",
    "notes": "First time UK visa application",
    "createDate": "2025-06-01T10:00:00",
    "updateDate": "2025-06-10T14:30:00",
    "applicants": [
      {
        "applicantId": 6001,
        "firstName": "John",
        "lastName": "Smith",
        "passportNumber": "AB1234567",
        "nationality": "AE",
        "nationalityName": "United Arab Emirates",
        "dateOfBirth": "1985-03-15",
        "gender": "M",
        "email": "john.smith@example.com",
        "phone": "+971501234567",
        "status": "DOCUMENTS_PENDING"
      },
      {
        "applicantId": 6002,
        "firstName": "Jane",
        "lastName": "Smith",
        "passportNumber": "AB7654321",
        "nationality": "AE",
        "nationalityName": "United Arab Emirates",
        "dateOfBirth": "1988-07-22",
        "gender": "F",
        "email": "jane.smith@example.com",
        "phone": "+971509876543",
        "status": "DOCUMENTS_COMPLETE"
      }
    ]
  }
}


POST /visa/writeapplication

Creates or updates a visa application.

Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | session | string | No | User session token | | applicationId | integer | No | Application ID (omit for create) | | customerId | integer | Yes | Customer ID | | destinationCountry | string | Yes | Destination country code | | visaType | string | Yes | Visa type (TOURIST, BUSINESS, TRANSIT, STUDENT, WORK) | | travelDate | string | No | Travel date (yyyy-MM-dd) | | returnDate | string | No | Return date (yyyy-MM-dd) | | status | string | No | Application status | | notes | string | No | Additional notes |

Request Example (Create):

{
  "session": "user-session-token",
  "customerId": 1002,
  "destinationCountry": "US",
  "visaType": "TOURIST",
  "travelDate": "2025-09-01",
  "returnDate": "2025-09-15",
  "notes": "Family vacation to Orlando"
}

Response Structure:

{
  "apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
  "apiData": {
    "applicationId": 5002,
    "applicationNumber": "VISA-2025-005002",
    "customerId": 1002,
    "customerName": "Michael Johnson",
    "destinationCountry": "US",
    "destinationCountryName": "United States",
    "visaType": "TOURIST",
    "travelDate": "2025-09-01",
    "returnDate": "2025-09-15",
    "status": "NEW",
    "notes": "Family vacation to Orlando",
    "createDate": "2025-06-15T10:30:00",
    "applicants": []
  }
}

Request Example (Update):

{
  "session": "user-session-token",
  "applicationId": 5002,
  "status": "IN_PROGRESS",
  "notes": "Family vacation to Orlando - expedited processing requested"
}


POST /visa/searchapplication

Searches visa applications.

Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | session | string | No | User session token | | customerId | integer | No | Filter by customer | | status | string | No | Filter by status | | destinationCountry | string | No | Filter by country | | fromDate | string | No | Filter from date | | toDate | string | No | Filter to date | | method | string | No | "partial" for partial matching |

Request Example:

{
  "session": "user-session-token",
  "status": "IN_PROGRESS",
  "fromDate": "2025-06-01",
  "toDate": "2025-06-30"
}

Response Structure:

{
  "apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
  "apiData": [
    {
      "applicationId": 5001,
      "applicationNumber": "VISA-2025-005001",
      "customerId": 1001,
      "customerName": "John Smith",
      "destinationCountry": "GB",
      "destinationCountryName": "United Kingdom",
      "visaType": "TOURIST",
      "travelDate": "2025-08-15",
      "status": "IN_PROGRESS",
      "createDate": "2025-06-01T10:00:00",
      "applicantCount": 2
    },
    {
      "applicationId": 5003,
      "applicationNumber": "VISA-2025-005003",
      "customerId": 1005,
      "customerName": "Sarah Williams",
      "destinationCountry": "FR",
      "destinationCountryName": "France",
      "visaType": "BUSINESS",
      "travelDate": "2025-07-20",
      "status": "IN_PROGRESS",
      "createDate": "2025-06-05T14:00:00",
      "applicantCount": 1
    }
  ]
}


Applicant Endpoints

POST /visa/readapplicant

Reads an applicant by ID.

Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | session | string | No | User session token | | applicantId | integer | Yes | Applicant ID |

Request Example:

{
  "session": "user-session-token",
  "applicantId": 6001
}

Response Structure:

{
  "apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
  "apiData": {
    "applicantId": 6001,
    "applicationId": 5001,
    "firstName": "John",
    "lastName": "Smith",
    "fullName": "John Smith",
    "passportNumber": "AB1234567",
    "passportIssueDate": "2020-01-15",
    "passportExpiryDate": "2030-01-14",
    "nationality": "AE",
    "nationalityName": "United Arab Emirates",
    "dateOfBirth": "1985-03-15",
    "placeOfBirth": "Dubai",
    "gender": "M",
    "email": "john.smith@example.com",
    "phone": "+971501234567",
    "occupation": "Software Engineer",
    "employer": "Tech Solutions LLC",
    "address": "123 Main Street, Dubai, UAE",
    "status": "DOCUMENTS_PENDING",
    "createDate": "2025-06-01T10:30:00",
    "documents": [
      {
        "documentId": 7001,
        "documentType": "PASSPORT_COPY",
        "fileName": "passport-john-smith.pdf",
        "status": "VERIFIED"
      },
      {
        "documentId": 7002,
        "documentType": "PHOTO",
        "fileName": "photo-john-smith.jpg",
        "status": "VERIFIED"
      },
      {
        "documentId": 7003,
        "documentType": "BANK_STATEMENT",
        "fileName": null,
        "status": "PENDING"
      }
    ]
  }
}


POST /visa/writeapplicant

Creates or updates an applicant.

Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | session | string | No | User session token | | applicantId | integer | No | Applicant ID (omit for create) | | applicationId | integer | Yes | Parent application ID | | firstName | string | Yes | First name | | lastName | string | Yes | Last name | | passportNumber | string | Yes | Passport number | | nationality | string | Yes | Nationality code | | dateOfBirth | string | Yes | Date of birth (yyyy-MM-dd) | | gender | string | No | Gender (M/F) | | email | string | No | Email address | | phone | string | No | Phone number | | occupation | string | No | Occupation | | employer | string | No | Employer name | | address | string | No | Address |

Request Example (Create):

{
  "session": "user-session-token",
  "applicationId": 5002,
  "firstName": "Michael",
  "lastName": "Johnson",
  "passportNumber": "CD9876543",
  "nationality": "AE",
  "dateOfBirth": "1980-05-20",
  "gender": "M",
  "email": "michael.johnson@example.com",
  "phone": "+971505551234",
  "occupation": "Business Owner",
  "employer": "Johnson Enterprises"
}

Response Structure:

{
  "apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
  "apiData": {
    "applicantId": 6003,
    "applicationId": 5002,
    "firstName": "Michael",
    "lastName": "Johnson",
    "fullName": "Michael Johnson",
    "passportNumber": "CD9876543",
    "nationality": "AE",
    "nationalityName": "United Arab Emirates",
    "dateOfBirth": "1980-05-20",
    "gender": "M",
    "email": "michael.johnson@example.com",
    "phone": "+971505551234",
    "occupation": "Business Owner",
    "employer": "Johnson Enterprises",
    "status": "DOCUMENTS_PENDING",
    "createDate": "2025-06-15T11:00:00",
    "documents": []
  }
}


POST /visa/searchapplicant

Searches applicants.

Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | session | string | No | User session token | | applicationId | integer | No | Filter by application | | passportNumber | string | No | Filter by passport | | lastName | string | No | Filter by last name | | method | string | No | "partial" for partial matching |

Request Example:

{
  "session": "user-session-token",
  "lastName": "Smith",
  "method": "partial"
}

Response Structure:

{
  "apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
  "apiData": [
    {
      "applicantId": 6001,
      "applicationId": 5001,
      "firstName": "John",
      "lastName": "Smith",
      "passportNumber": "AB1234567",
      "nationality": "AE",
      "dateOfBirth": "1985-03-15",
      "status": "DOCUMENTS_PENDING"
    },
    {
      "applicantId": 6002,
      "applicationId": 5001,
      "firstName": "Jane",
      "lastName": "Smith",
      "passportNumber": "AB7654321",
      "nationality": "AE",
      "dateOfBirth": "1988-07-22",
      "status": "DOCUMENTS_COMPLETE"
    }
  ]
}


Document Endpoints

POST /visa/readdocument

Reads a document by ID.

Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | session | string | No | User session token | | documentId | integer | Yes | Document ID |

Request Example:

{
  "session": "user-session-token",
  "documentId": 7001
}

Response Structure:

{
  "apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
  "apiData": {
    "documentId": 7001,
    "applicantId": 6001,
    "applicantName": "John Smith",
    "documentType": "PASSPORT_COPY",
    "documentTypeName": "Passport Copy",
    "fileName": "passport-john-smith.pdf",
    "fileUrl": "https://storage.example.com/visa-docs/7001-passport.pdf",
    "fileSize": 245678,
    "status": "VERIFIED",
    "expiryDate": null,
    "uploadedAt": "2025-06-02T09:00:00",
    "verifiedAt": "2025-06-02T14:30:00",
    "verifiedBy": "Agent Jane",
    "notes": null
  }
}


POST /visa/writedocument

Creates or updates a document.

Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | session | string | No | User session token | | documentId | integer | No | Document ID (omit for create) | | applicantId | integer | Yes | Parent applicant ID | | documentType | string | Yes | Document type | | fileName | string | Yes | File name | | fileUrl | string | Yes | File URL/path | | status | string | No | Document status | | expiryDate | string | No | Expiry date |

Request Example (Create):

{
  "session": "user-session-token",
  "applicantId": 6001,
  "documentType": "BANK_STATEMENT",
  "fileName": "bank-statement-john-smith.pdf",
  "fileUrl": "https://storage.example.com/visa-docs/bank-statement-6001.pdf"
}

Response Structure:

{
  "apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
  "apiData": {
    "documentId": 7004,
    "applicantId": 6001,
    "documentType": "BANK_STATEMENT",
    "documentTypeName": "Bank Statement",
    "fileName": "bank-statement-john-smith.pdf",
    "fileUrl": "https://storage.example.com/visa-docs/bank-statement-6001.pdf",
    "status": "UPLOADED",
    "uploadedAt": "2025-06-15T11:30:00"
  }
}

Request Example (Update status):

{
  "session": "user-session-token",
  "documentId": 7004,
  "status": "VERIFIED"
}


POST /visa/searchdocument

Searches documents.

Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | session | string | No | User session token | | applicantId | integer | No | Filter by applicant | | documentType | string | No | Filter by type | | status | string | No | Filter by status |

Request Example:

{
  "session": "user-session-token",
  "applicantId": 6001,
  "status": "VERIFIED"
}

Response Structure:

{
  "apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
  "apiData": [
    {
      "documentId": 7001,
      "applicantId": 6001,
      "documentType": "PASSPORT_COPY",
      "documentTypeName": "Passport Copy",
      "fileName": "passport-john-smith.pdf",
      "status": "VERIFIED",
      "uploadedAt": "2025-06-02T09:00:00"
    },
    {
      "documentId": 7002,
      "applicantId": 6001,
      "documentType": "PHOTO",
      "documentTypeName": "Photograph",
      "fileName": "photo-john-smith.jpg",
      "status": "VERIFIED",
      "uploadedAt": "2025-06-02T09:15:00"
    }
  ]
}


Delivery Endpoints

POST /visa/readdelivery

Reads a delivery by ID.

Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | session | string | No | User session token | | deliveryId | integer | Yes | Delivery ID |

Request Example:

{
  "session": "user-session-token",
  "deliveryId": 8001
}

Response Structure:

{
  "apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
  "apiData": {
    "deliveryId": 8001,
    "applicationId": 5001,
    "applicationNumber": "VISA-2025-005001",
    "deliveryType": "COURIER",
    "address": "123 Main Street, Downtown Dubai, UAE",
    "city": "Dubai",
    "postalCode": "12345",
    "contactName": "John Smith",
    "contactPhone": "+971501234567",
    "scheduledDate": "2025-08-10T10:00:00",
    "status": "PENDING",
    "trackingNumber": null,
    "courierCompany": "Aramex",
    "createdAt": "2025-06-10T15:00:00",
    "notes": "Call before delivery"
  }
}


POST /visa/writedelivery

Creates or updates a delivery.

Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | session | string | No | User session token | | deliveryId | integer | No | Delivery ID (omit for create) | | applicationId | integer | Yes | Application ID | | deliveryType | string | Yes | Delivery type (PICKUP, COURIER) | | address | string | No | Delivery address | | contactName | string | No | Contact name | | contactPhone | string | No | Contact phone | | scheduledDate | string | No | Scheduled date | | status | string | No | Delivery status |

Request Example (Create):

{
  "session": "user-session-token",
  "applicationId": 5002,
  "deliveryType": "PICKUP",
  "scheduledDate": "2025-08-25T14:00:00",
  "contactName": "Michael Johnson",
  "contactPhone": "+971505551234"
}

Response Structure:

{
  "apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
  "apiData": {
    "deliveryId": 8002,
    "applicationId": 5002,
    "applicationNumber": "VISA-2025-005002",
    "deliveryType": "PICKUP",
    "contactName": "Michael Johnson",
    "contactPhone": "+971505551234",
    "scheduledDate": "2025-08-25T14:00:00",
    "status": "SCHEDULED",
    "createdAt": "2025-06-15T12:00:00"
  }
}


POST /visa/searchdelivery

Searches deliveries.

Request Example:

{
  "session": "user-session-token",
  "status": "PENDING"
}

Response Structure:

{
  "apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
  "apiData": [
    {
      "deliveryId": 8001,
      "applicationId": 5001,
      "applicationNumber": "VISA-2025-005001",
      "deliveryType": "COURIER",
      "contactName": "John Smith",
      "scheduledDate": "2025-08-10T10:00:00",
      "status": "PENDING"
    }
  ]
}


Invoice Endpoints

POST /visa/readinvoice

Reads an invoice by ID.

Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | session | string | No | User session token | | invoiceId | integer | Yes | Invoice ID |

Request Example:

{
  "session": "user-session-token",
  "invoiceId": 9001
}

Response Structure:

{
  "apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
  "apiData": {
    "invoiceId": 9001,
    "invoiceNumber": "VINV-2025-009001",
    "applicationId": 5001,
    "applicationNumber": "VISA-2025-005001",
    "customerId": 1001,
    "customerName": "John Smith",
    "amount": 850.00,
    "currency": "AED",
    "status": "PAID",
    "dueDate": "2025-06-15",
    "paidDate": "2025-06-10",
    "paidAmount": 850.00,
    "paymentMethod": "CARD",
    "createdAt": "2025-06-01T10:30:00",
    "lineItems": [
      {
        "description": "UK Tourist Visa - Adult",
        "quantity": 2,
        "unitPrice": 350.00,
        "amount": 700.00
      },
      {
        "description": "Processing Fee",
        "quantity": 1,
        "unitPrice": 100.00,
        "amount": 100.00
      },
      {
        "description": "Courier Delivery",
        "quantity": 1,
        "unitPrice": 50.00,
        "amount": 50.00
      }
    ]
  }
}


POST /visa/writeinvoice

Creates or updates an invoice.

Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | session | string | No | User session token | | invoiceId | integer | No | Invoice ID (omit for create) | | applicationId | integer | Yes | Application ID | | amount | number | Yes | Invoice amount | | currency | string | No | Currency code | | status | string | No | Invoice status | | dueDate | string | No | Due date | | paidDate | string | No | Payment date |

Request Example (Create):

{
  "session": "user-session-token",
  "applicationId": 5002,
  "amount": 1500.00,
  "currency": "AED",
  "dueDate": "2025-06-30"
}

Response Structure:

{
  "apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
  "apiData": {
    "invoiceId": 9002,
    "invoiceNumber": "VINV-2025-009002",
    "applicationId": 5002,
    "amount": 1500.00,
    "currency": "AED",
    "status": "PENDING",
    "dueDate": "2025-06-30",
    "createdAt": "2025-06-15T12:30:00"
  }
}


POST /visa/searchinvoice

Searches invoices.

Request Example:

{
  "session": "user-session-token",
  "status": "PENDING"
}

Response Structure:

{
  "apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
  "apiData": [
    {
      "invoiceId": 9002,
      "invoiceNumber": "VINV-2025-009002",
      "applicationId": 5002,
      "customerName": "Michael Johnson",
      "amount": 1500.00,
      "currency": "AED",
      "status": "PENDING",
      "dueDate": "2025-06-30"
    }
  ]
}


Requirement Lookup Endpoints

POST /visa/lookuprequirement

Looks up visa requirements for a passport nationality and destination country pair using the TravelBuddyAI RapidAPI (v2). Results are cached for 30 days in the DocumentrequirementcacheEntity table.

Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | session | string | No | User session token | | passport | string | Yes | Passport nationality ISO Alpha-2 code (e.g. "TR") | | destination | string | Yes | Destination country ISO Alpha-2 code (e.g. "KR") |

Request Example:

{
  "session": "user-session-token",
  "passport": "TR",
  "destination": "KR"
}

Response Structure: Returns the raw TravelBuddyAI API response (after stripping the outer data wrapper). The response structure follows the TravelBuddyAI v2 format:

{
  "apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
  "apiData": {
    "passport": { "code": "TR", "name": "Turkiye", "currency_code": "TRY" },
    "destination": { "code": "KR", "name": "South Korea", "continent": "Asia", ... },
    "visa_rules": {
      "primary_rule": {
        "name": "eTA",
        "duration": "90 days",
        "color": "yellow"
      }
    },
    "mandatory_registration": {
      "name": "e-Arrival",
      "color": "yellow",
      "link": "...",
      "comment": "3 days before departure"
    }
  }
}

Classification: The frontend classifies the visa_rules.primary_rule.name value into visa categories using a data-driven rules array. See Visa Category Classification below.

Same-country optimization: If passport equals destination, the backend returns a synthetic "No visa required" response immediately without calling the external API.

Caching: Results are cached in nts.documentrequirementcache (JSONB) with a 30-day TTL. Stale entries are refreshed on next lookup.


Requirement Endpoints

POST /visa/readrequirement

Reads a requirement by ID.

Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | session | string | No | User session token | | requirementId | integer | Yes | Requirement ID |

Request Example:

{
  "session": "user-session-token",
  "requirementId": 10001
}

Response Structure:

{
  "apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
  "apiData": {
    "requirementId": 10001,
    "country": "GB",
    "countryName": "United Kingdom",
    "nationality": "AE",
    "nationalityName": "United Arab Emirates",
    "visaType": "TOURIST",
    "documentType": "PASSPORT_COPY",
    "documentTypeName": "Passport Copy",
    "mandatory": true,
    "description": "Clear color copy of passport bio-data page. Passport must be valid for at least 6 months from travel date.",
    "validityRequirement": "6 months from travel date",
    "additionalNotes": "Both front and back cover required for some nationalities"
  }
}


POST /visa/writerequirement

Creates or updates a requirement.

Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | session | string | No | User session token | | requirementId | integer | No | Requirement ID (omit for create) | | country | string | Yes | Destination country | | nationality | string | Yes | Applicant nationality | | visaType | string | Yes | Visa type | | documentType | string | Yes | Required document type | | mandatory | boolean | Yes | Whether required | | description | string | No | Requirement description |

Request Example:

{
  "session": "user-session-token",
  "country": "US",
  "nationality": "AE",
  "visaType": "TOURIST",
  "documentType": "EMPLOYMENT_LETTER",
  "mandatory": true,
  "description": "Letter from employer confirming employment, position, salary, and approved leave dates"
}

Response Structure:

{
  "apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
  "apiData": {
    "requirementId": 10050,
    "country": "US",
    "countryName": "United States",
    "nationality": "AE",
    "nationalityName": "United Arab Emirates",
    "visaType": "TOURIST",
    "documentType": "EMPLOYMENT_LETTER",
    "documentTypeName": "Employment Letter",
    "mandatory": true,
    "description": "Letter from employer confirming employment, position, salary, and approved leave dates",
    "createdAt": "2025-06-15T13:00:00"
  }
}


POST /visa/searchrequirement

Searches requirements.

Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | session | string | No | User session token | | country | string | No | Filter by country | | nationality | string | No | Filter by nationality | | visaType | string | No | Filter by visa type |

Request Example:

{
  "session": "user-session-token",
  "country": "GB",
  "nationality": "AE",
  "visaType": "TOURIST"
}

Response Structure:

{
  "apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
  "apiData": [
    {
      "requirementId": 10001,
      "country": "GB",
      "nationality": "AE",
      "visaType": "TOURIST",
      "documentType": "PASSPORT_COPY",
      "documentTypeName": "Passport Copy",
      "mandatory": true,
      "description": "Clear color copy of passport bio-data page"
    },
    {
      "requirementId": 10002,
      "country": "GB",
      "nationality": "AE",
      "visaType": "TOURIST",
      "documentType": "PHOTO",
      "documentTypeName": "Photograph",
      "mandatory": true,
      "description": "Recent passport-size photograph (35x45mm, white background)"
    },
    {
      "requirementId": 10003,
      "country": "GB",
      "nationality": "AE",
      "visaType": "TOURIST",
      "documentType": "BANK_STATEMENT",
      "documentTypeName": "Bank Statement",
      "mandatory": true,
      "description": "Last 6 months bank statements showing sufficient funds"
    },
    {
      "requirementId": 10004,
      "country": "GB",
      "nationality": "AE",
      "visaType": "TOURIST",
      "documentType": "EMPLOYMENT_LETTER",
      "documentTypeName": "Employment Letter",
      "mandatory": true,
      "description": "Letter from employer with job title, salary, and leave approval"
    },
    {
      "requirementId": 10005,
      "country": "GB",
      "nationality": "AE",
      "visaType": "TOURIST",
      "documentType": "TRAVEL_ITINERARY",
      "documentTypeName": "Travel Itinerary",
      "mandatory": false,
      "description": "Flight and hotel bookings (can be tentative)"
    }
  ]
}


Data Models

CVisaApplication

Field Type Description
applicationId integer Application ID
applicationNumber string Reference number
customerId integer Customer ID
customerName string Customer name
destinationCountry string Destination country code
destinationCountryName string Destination country name
visaType string Visa type (TOURIST, BUSINESS, TRANSIT, STUDENT, WORK)
travelDate date Travel date
returnDate date Return date
status string Status (NEW, IN_PROGRESS, SUBMITTED, APPROVED, REJECTED, CANCELLED)
notes string Additional notes
applicants array List of applicants
createDate datetime Creation date
updateDate datetime Last update date

CVisaApplicant

Field Type Description
applicantId integer Applicant ID
applicationId integer Parent application
firstName string First name
lastName string Last name
fullName string Full name
passportNumber string Passport number
passportIssueDate date Passport issue date
passportExpiryDate date Passport expiry date
nationality string Nationality code
nationalityName string Nationality name
dateOfBirth date Date of birth
placeOfBirth string Place of birth
gender string Gender (M/F)
email string Email
phone string Phone
occupation string Occupation
employer string Employer name
address string Address
status string Status (DOCUMENTS_PENDING, DOCUMENTS_COMPLETE, SUBMITTED, APPROVED, REJECTED)
documents array List of documents
createDate datetime Creation date

CVisaDocument

Field Type Description
documentId integer Document ID
applicantId integer Parent applicant
documentType string Document type code
documentTypeName string Document type name
fileName string File name
fileUrl string File URL/path
fileSize integer File size in bytes
status string Status (PENDING, UPLOADED, VERIFIED, REJECTED)
expiryDate date Expiry date
uploadedAt datetime Upload timestamp
verifiedAt datetime Verification timestamp
verifiedBy string Verifier name
notes string Notes

CVisaDelivery

Field Type Description
deliveryId integer Delivery ID
applicationId integer Application ID
applicationNumber string Application number
deliveryType string Type (PICKUP, COURIER)
address string Delivery address
city string City
postalCode string Postal code
contactName string Contact name
contactPhone string Contact phone
scheduledDate datetime Scheduled date
status string Status (SCHEDULED, PENDING, IN_TRANSIT, DELIVERED, CANCELLED)
trackingNumber string Courier tracking number
courierCompany string Courier company name
notes string Notes
createdAt datetime Creation timestamp

CVisaInvoice

Field Type Description
invoiceId integer Invoice ID
invoiceNumber string Invoice number
applicationId integer Application ID
applicationNumber string Application number
customerId integer Customer ID
customerName string Customer name
amount decimal Invoice amount
currency string Currency code
status string Status (PENDING, PAID, CANCELLED, REFUNDED)
dueDate date Due date
paidDate date Payment date
paidAmount decimal Amount paid
paymentMethod string Payment method
lineItems array Invoice line items
createdAt datetime Creation timestamp

CVisaRequirement

Field Type Description
requirementId integer Requirement ID
country string Destination country code
countryName string Destination country name
nationality string Applicant nationality code
nationalityName string Applicant nationality name
visaType string Visa type
documentType string Required document type code
documentTypeName string Required document type name
mandatory boolean Is mandatory
description string Description
validityRequirement string Validity requirement
additionalNotes string Additional notes
createdAt datetime Creation timestamp

Secure Document Storage Endpoints (TQ-18)

These endpoints replace the legacy document/upload2 file-system storage with encrypted S3 storage for visa documents.

POST /visa/document/upload

Upload a visa document to encrypted S3 storage. Combines file upload and metadata save into one atomic call. The file is Base64-encoded in the JSON body (same pattern as legacy upload3()).

Access: agent, admin

Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | session | string | No | User session token | | visaApplicationId | integer | Yes | Application ID | | docDescription | string | No | Document description (e.g. "Passport copy") | | fileToUpload | string | Yes | Base64-encoded file content | | fileName | string | Yes | Original filename (e.g. "passport.pdf") |

Response: Saved CVisaDocument object with docUrl containing the S3 key.

Processing: 1. MIME type detected from file bytes (Tika) -- extension and Content-Type ignored 2. Validates: only PDF, JPEG, PNG allowed; max 10 MB 3. Uploads to S3 with SSE-KMS encryption 4. S3 key: applications/{applicationId}/documents/{8-hex-suffix}.{ext}


POST /visa/document/download

Download a visa document directly (binary response). For agent use only -- no presigned URLs needed.

Access: agent, admin

Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | session | string | No | User session token | | visaDocumentId | integer | Yes | Document ID |

Response: Binary file with Content-Disposition: attachment and appropriate Content-Type.

Backward compatibility: If docUrl is a legacy local path (not an S3 key), the file is read from the local filesystem.


Visa Delivery Endpoints (TQ-18)

Agent endpoints for initiating visa delivery and notifying customers.

POST /visa/delivery/initiate

Upload the issued visa PDF to encrypted S3 and create a delivery record.

Access: agent, admin

Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | session | string | No | User session token | | visaApplicationId | integer | Yes | Application ID | | fileToUpload | string | Yes | Base64-encoded visa PDF | | fileName | string | No | Filename (defaults to "visa.pdf") |

Response: Created CVisaDelivery object.


POST /visa/delivery/notify

Send download notification to the customer via email, SMS, or WhatsApp.

Access: agent, admin

Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | session | string | No | User session token | | visaDeliveryId | integer | Yes | Delivery ID | | channel | string | Yes | email, sms, or whatsapp |

Response: Updated CVisaDelivery with emailSentDate or smsSentDate set.


Public Customer Download Endpoints (TQ-18)

These endpoints are publicly accessible (guest role) and power the visa-download.html page. No Keycloak authentication required.

POST /visa/delivery/verify

Customer provides email and phone; server validates against the application record.

Access: guest, agent, admin

Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | ref | string | Yes | Application public GUID (from URL parameter) | | email | string | Yes | Customer email | | phone | string | Yes | Customer phone number |

Response (match):

{
  "apiData": {
    "matched": true,
    "maskedEmail": "c***r@example.com",
    "maskedPhone": "+971*****567",
    "channels": ["email", "sms", "whatsapp"]
  }
}

Response (no match):

{
  "apiData": { "matched": false }
}

Rate limiting: Max 5 verification attempts per delivery, then 15-minute lockout.


POST /visa/delivery/sendotp

Send a one-time verification code via the customer's chosen channel.

Access: guest, agent, admin

Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | ref | string | Yes | Application public GUID | | channel | string | Yes | email, sms, or whatsapp |

Response:

{
  "apiData": {
    "sent": true,
    "maskedDestination": "+971*****567",
    "expiresInMinutes": 10
  }
}


POST /visa/delivery/validateotp

Validate the OTP code. If valid, returns a time-limited presigned S3 download URL.

Access: guest, agent, admin

Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | ref | string | Yes | Application public GUID | | otp | string | Yes | 6-digit verification code |

Response (valid):

{
  "apiData": {
    "valid": true,
    "downloadUrl": "https://tq-visa-documents.s3.ap-south-1.amazonaws.com/...",
    "fileName": "visa-42.pdf",
    "expiresInMinutes": 15
  }
}

Response (invalid):

{
  "apiData": {
    "valid": false,
    "reason": "expired"
  }
}

Possible reasons: invalid, expired, max_attempts

Rate limiting: Max 3 OTP validation attempts. After 3 failures, a new OTP must be requested.


Visa Category Classification

Both the backend (GroupManagerFacade.classifyVisaCategory) and frontend (outbound-groups-passengers.js) use a data-driven rules array to classify the TravelBuddyAI API response into visa categories. The classification extracts visa_rules.primary_rule.name from the response (with fallback to the legacy category field) and matches it against keywords.

Rules array (order matters — first match wins):

Category Keywords
NOT_REQUIRED "visa free", "freedom of movement", "no visa required"
VOA "visa on arrival"
ONLINE "e-visa", "online visa", "eta", "electronic travel"
REQUIRED "visa required"

If no keyword matches, the category defaults to UNKNOWN.

Notes: - The match uses case-insensitive contains, so "Online visa required" matches "online visa" (ONLINE) before "visa required" (REQUIRED) because ONLINE appears earlier in the rules array. - "eTA" (Electronic Travel Authorization) is classified as ONLINE since it is an online pre-travel authorization similar to e-visa. - The legacy category field (from TravelBuddyAI v1) is checked as a fallback if visa_rules.primary_rule.name is not present. - When passport and destination country are the same, the system short-circuits with NOT_REQUIRED without calling the external API.