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:
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:
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:
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:
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:
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):
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:
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:
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:
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:
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:
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:
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:
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:
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) |
| string | ||
| 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):
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:
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):
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.