/** * Hostex API Client * Main client for interacting with the Hostex API v3 */ import axios, { AxiosInstance, AxiosRequestConfig, AxiosError } from 'axios'; import { HostexApiResponse, PropertiesData, PropertiesQueryParams, CreatePropertyParams, CreatePropertyData, RoomTypesData, RoomTypesQueryParams, CreateRoomTypeParams, CreateRoomTypeData, ReservationsData, ReservationsQueryParams, CreateReservationParams, CreateReservationData, UpdateCheckInDetailsParams, UpdateReservationBasicInfoParams, UpdateStayStatusParams, AddTagParams, RemoveTagParams, MoveToBoxParams, AllocateToPropertyParams, UpdateCustomFieldsParams, CustomFieldsData, CustomChannelsData, IncomeMethodsData, AvailabilitiesData, AvailabilitiesQueryParams, UpdateAvailabilitiesParams, CalendarData, CalendarQueryParams, UpdateListingInventoriesParams, UpdateListingPricesParams, UpdateListingRestrictionsParams, ConversationsData, ConversationsQueryParams, ConversationDetailsData, SendMessageParams, ReviewsData, ReviewsQueryParams, CreateReviewParams, WebhooksData, CreateWebhookParams, UpdateWebhookParams, DeleteWebhookParams, OAuthTokenParams, OAuthTokenData, RevokeTokenParams, ChannelAccountsQueryParams, ChannelAccountsData, ListingsQueryParams, ListingsData, UpdateAirbnbPriceAndRulesParams, UpdateVrboPriceAndRulesParams, PricingRatiosQueryParams, PricingRatiosData, CalendarShareLinksQueryParams, CalendarShareLinksData, CreateCalendarShareLinkParams, CreateCalendarShareLinkData, UpdateConversationNoteParams, UpdateConversationNoteData, GroupsQueryParams, GroupsData, CreateGroupParams, CreateGroupData, UpdateGroupParams, TagsQueryParams, TagsData, CreateTagParams, CreateTagData, UpdateTagParams, ReservationTagsQueryParams, ReservationTagsData, CreateReservationTagParams, CreateReservationTagData, StaffsQueryParams, StaffsData, CreateStaffParams, CreateStaffData, UpdateStaffParams, TasksQueryParams, TasksData, CreateTaskParams, CreateTaskData, UpdateTaskParams, IncomeItemsData, ExpenseItemsData, ExpenseMethodsData, TransactionsQueryParams, TransactionsData, CreateTransactionParams, CreateTransactionData, UpdateTransactionParams, KnowledgeBasesQueryParams, KnowledgeBasesData, KnowledgeBaseDetail, CreateKnowledgeBaseParams, UpdateKnowledgeBaseParams, AutomationActionsQueryParams, AutomationActionsData, } from '../types/index.js'; import { HostexApiError, createErrorFromResponse } from '../utils/error.js'; import { withRetry, RetryOptions } from '../utils/retry.js'; import { validateDateRange, validateRequired, validateNonEmptyArray, validateUrl, } from '../utils/validation.js'; import { RateLimiter, type RateLimiterConfig } from '../rate-limiter/index.js'; /** * Client configuration options */ export interface HostexApiClientConfig { /** Hostex access token */ accessToken: string; /** API base URL (default: https://api.hostex.io/v3) */ baseUrl?: string; /** Request timeout in milliseconds (default: 30000) */ timeout?: number; /** Retry options */ retry?: RetryOptions; /** Throttle options for mutation requests to avoid rate limiting */ throttle?: { /** Minimum delay in ms between mutation requests (POST/PUT/PATCH/DELETE). Default: 0 (no throttle) */ minDelayMs?: number; }; /** Rate limiter — either config to create one, or a pre-built RateLimiter instance */ rateLimiter?: RateLimiterConfig | RateLimiter; } /** * Hostex API Client */ export class HostexApiClient { private readonly axios: AxiosInstance; private readonly retryOptions: RetryOptions; private readonly throttleMinDelayMs: number; private readonly rateLimiter?: RateLimiter; private lastMutationTime = 0; private throttleQueue: Promise = Promise.resolve(); constructor(config: HostexApiClientConfig) { validateRequired(config.accessToken, 'accessToken'); this.retryOptions = config.retry || {}; this.throttleMinDelayMs = config.throttle?.minDelayMs ?? 0; if (config.rateLimiter) { this.rateLimiter = config.rateLimiter instanceof RateLimiter ? config.rateLimiter : new RateLimiter(config.rateLimiter); } this.axios = axios.create({ baseURL: config.baseUrl || 'https://api.hostex.io/v3', timeout: config.timeout || 30000, headers: { 'accept': 'application/json', 'Content-Type': 'application/json', 'Hostex-Access-Token': config.accessToken, }, }); this.setupInterceptors(); } private static readonly MUTATION_METHODS = new Set(['post', 'put', 'patch', 'delete']); private static readonly RATE_LIMIT_RETRY_DELAY_MS = 65_000; /** * Setup axios interceptors for throttling and error handling */ private setupInterceptors(): void { // Request interceptor: cross-process rate limiting via Redis if (this.rateLimiter) { const limiter = this.rateLimiter; this.axios.interceptors.request.use(async (config) => { const method = (config.method || 'get').toUpperCase(); const url = config.url || ''; await limiter.acquire(method, url); return config; }); } // Request interceptor: throttle mutation requests to stay under rate limits. // Uses a promise chain so concurrent callers serialize properly — // even if two cron ticks overlap, only one mutation fires at a time. this.axios.interceptors.request.use(async (config) => { const method = (config.method || 'get').toLowerCase(); if (this.throttleMinDelayMs > 0 && HostexApiClient.MUTATION_METHODS.has(method)) { await new Promise((resolve) => { this.throttleQueue = this.throttleQueue.then(async () => { const elapsed = Date.now() - this.lastMutationTime; const wait = this.throttleMinDelayMs - elapsed; if (wait > 0) { await new Promise((r) => setTimeout(r, wait)); } this.lastMutationTime = Date.now(); resolve(); }); }); } return config; }); // Response interceptor: error handling + one-time retry on rate limit this.axios.interceptors.response.use( (response) => { // Check for Hostex API error codes const data = response.data as HostexApiResponse; if (data.error_code && data.error_code !== 200) { throw createErrorFromResponse(data, response.status); } return response; }, async (error: AxiosError) => { // Detect rate limiting: HTTP 429 or "Too Many Attempts" message if (this.isRateLimitError(error) && !((error.config as any)?._rateLimitRetried)) { const config = error.config; if (config) { (config as any)._rateLimitRetried = true; await new Promise((resolve) => setTimeout(resolve, HostexApiClient.RATE_LIMIT_RETRY_DELAY_MS) ); // Reset mutation timestamp so throttle doesn't double-wait this.lastMutationTime = 0; return this.axios.request(config); } } if (error.response) { const data = error.response.data as HostexApiResponse; throw createErrorFromResponse(data, error.response.status); } // Network or timeout error throw new HostexApiError( error.message || 'Network error', 0, undefined, error.code === 'ECONNABORTED' ? 408 : 500 ); } ); } /** * Check if an error is a rate limit ("Too Many Attempts") error */ private isRateLimitError(error: AxiosError): boolean { if (error.response?.status === 429) return true; const data = error.response?.data; if (typeof data === 'object' && data !== null) { const message = (data as any).message || (data as any).error_message || ''; if (typeof message === 'string' && message.toLowerCase().includes('too many attempts')) { return true; } } return false; } /** * Make a request with retry logic */ private async request(config: AxiosRequestConfig): Promise { return withRetry(async () => { const response = await this.axios.request>(config); return response.data.data as T; }, this.retryOptions); } // ===================== // Properties API // ===================== /** * Query properties * @see https://docs.hostex.io/reference/query-properties */ async getProperties(params?: PropertiesQueryParams): Promise { return this.request({ method: 'GET', url: '/properties', params, }); } /** * Get all properties (handles pagination automatically) */ async getAllProperties(): Promise { const allProperties: PropertiesData = { properties: [], total: 0 }; let offset = 0; const limit = 100; while (true) { const data = await this.getProperties({ offset, limit }); allProperties.properties.push(...data.properties); allProperties.total = data.total; if (data.properties.length < limit) break; offset += limit; } return allProperties; } /** * Create a property * @see https://docs.hostex.io/reference/create-property */ async createProperty(params: CreatePropertyParams): Promise> { validateRequired(params.title, 'title'); const response = await this.axios.post>('/properties', params); return response.data; } // ===================== // Room Types API // ===================== /** * Query room types * @see https://docs.hostex.io/reference/query-room-types */ async getRoomTypes(params?: RoomTypesQueryParams): Promise { return this.request({ method: 'GET', url: '/room_types', params: { ...params, offset: params?.offset ?? 0, limit: params?.limit ?? 20, }, }); } /** * Get all room types (handles pagination automatically) */ async getAllRoomTypes(): Promise { const allRoomTypes: RoomTypesData = { room_types: [], total: 0 }; let offset = 0; const limit = 100; while (true) { const data = await this.getRoomTypes({ offset, limit }); allRoomTypes.room_types.push(...data.room_types); allRoomTypes.total = data.total; if (data.room_types.length < limit) break; offset += limit; } return allRoomTypes; } /** * Create a room type * @see https://docs.hostex.io/reference/create-roomtype */ async createRoomType(params: CreateRoomTypeParams): Promise> { validateRequired(params.title, 'title'); const response = await this.axios.post>('/room_types', params); return response.data; } // ===================== // Reservations API // ===================== /** * Query reservations * @see https://docs.hostex.io/reference/query-reservations */ async getReservations(params?: ReservationsQueryParams): Promise { return this.request({ method: 'GET', url: '/reservations', params, }); } /** * Create a reservation (direct booking) * @see https://docs.hostex.io/reference/create-reservation */ async createReservation(params: CreateReservationParams): Promise> { validateRequired(params.property_id, 'property_id'); validateRequired(params.custom_channel_id, 'custom_channel_id'); validateRequired(params.check_in_date, 'check_in_date'); validateRequired(params.check_out_date, 'check_out_date'); validateRequired(params.guest_name, 'guest_name'); validateRequired(params.currency, 'currency'); validateRequired(params.rate_amount, 'rate_amount'); validateRequired(params.commission_amount, 'commission_amount'); validateRequired(params.received_amount, 'received_amount'); validateRequired(params.income_method_id, 'income_method_id'); validateDateRange(params.check_in_date, params.check_out_date); const response = await this.axios.post>('/reservations', params); return response.data; } /** * Cancel a reservation (direct booking only) * @see https://docs.hostex.io/reference/cancel-reservation */ async cancelReservation(reservationCode: string): Promise { validateRequired(reservationCode, 'reservationCode'); const response = await this.axios.delete( `/reservations/${reservationCode}` ); return response.data; } /** * Approve a pending reservation request * The reservation must be in `wait_accept` status. * @see https://docs.hostex.io/reference/approve-reservation */ async approveReservation(reservationCode: string): Promise { validateRequired(reservationCode, 'reservationCode'); const response = await this.axios.post( `/reservations/${reservationCode}/approve` ); return response.data; } /** * Decline a pending reservation request * The reservation must be in `wait_accept` status. * @see https://docs.hostex.io/reference/decline-reservation */ async declineReservation(reservationCode: string): Promise { validateRequired(reservationCode, 'reservationCode'); const response = await this.axios.post( `/reservations/${reservationCode}/decline` ); return response.data; } /** * Update reservation basic info * @see https://docs.hostex.io/reference/update-reservation-basic-info */ async updateReservationBasicInfo(params: UpdateReservationBasicInfoParams): Promise { const { stay_code, reservation_code, ...body } = params; const code = stay_code ?? reservation_code; validateRequired(code, 'stay_code'); const response = await this.axios.patch( `/reservations/${code}`, body ); return response.data; } /** * Update check-in details for a stay * @see https://docs.hostex.io/reference/update-check-in-details */ async updateCheckInDetails(params: UpdateCheckInDetailsParams): Promise { validateRequired(params.stay_code, 'stay_code'); const { stay_code, ...body } = params; const response = await this.axios.patch( `/reservations/${stay_code}/check_in_details`, body ); return response.data; } /** * Update stay status * @see https://docs.hostex.io/reference/update-stay-status */ async updateStayStatus(params: UpdateStayStatusParams): Promise { validateRequired(params.stay_code, 'stay_code'); validateRequired(params.stay_status, 'stay_status'); const response = await this.axios.put( `/reservations/${params.stay_code}/stay_status`, { stay_status: params.stay_status } ); return response.data; } /** * Add a tag to a reservation * @see https://docs.hostex.io/reference/add-tag */ async addTag(params: AddTagParams): Promise { validateRequired(params.stay_code, 'stay_code'); validateRequired(params.tag_name, 'tag_name'); const response = await this.axios.post( `/reservations/${params.stay_code}/tags`, { tag_name: params.tag_name } ); return response.data; } /** * Remove a tag from a reservation * @see https://docs.hostex.io/reference/remove-tag */ async removeTag(params: RemoveTagParams): Promise { validateRequired(params.stay_code, 'stay_code'); validateRequired(params.tag_name, 'tag_name'); const response = await this.axios.delete( `/reservations/${params.stay_code}/tags`, { data: { tag_name: params.tag_name } } ); return response.data; } /** * Move a stay to the reservation box * @see https://docs.hostex.io/reference/move-reservation-to-box */ async moveToBox(params: MoveToBoxParams): Promise { validateRequired(params.stay_code, 'stay_code'); const response = await this.axios.post( `/reservations/${params.stay_code}/move_to_box` ); return response.data; } /** * Allocate a reservation to a property * @see https://docs.hostex.io/reference/allocate-reservation */ async allocateToProperty(params: AllocateToPropertyParams): Promise { validateRequired(params.stay_code, 'stay_code'); validateRequired(params.property_id, 'property_id'); const response = await this.axios.post( `/reservations/${params.stay_code}/allocate`, { property_id: params.property_id } ); return response.data; } /** * Query custom fields for a stay * @see https://docs.hostex.io/reference/query-custom-fields */ async getCustomFields(stayCode: string): Promise { validateRequired(stayCode, 'stayCode'); return this.request({ method: 'GET', url: `/reservations/${stayCode}/custom_fields`, }); } /** * Update custom fields for a stay * @see https://docs.hostex.io/reference/update-custom-fields */ async updateCustomFields(params: UpdateCustomFieldsParams): Promise { validateRequired(params.stay_code, 'stay_code'); validateRequired(params.custom_fields, 'custom_fields'); return this.request({ method: 'PATCH', url: `/reservations/${params.stay_code}/custom_fields`, data: { custom_fields: params.custom_fields }, }); } /** * Query custom channels * @see https://docs.hostex.io/reference/query-custom-channels */ async getCustomChannels(): Promise { return this.request({ method: 'GET', url: '/custom_channels', }); } /** * Query income methods * @see https://docs.hostex.io/reference/query-income-methods */ async getIncomeMethods(): Promise { return this.request({ method: 'GET', url: '/income_methods', }); } // ===================== // Availability API // ===================== /** * Query property availabilities * @see https://docs.hostex.io/reference/query-availabilities */ async getAvailabilities(params: AvailabilitiesQueryParams): Promise { const propertyIds = Array.isArray(params.property_ids) ? params.property_ids.join(',') : params.property_ids; validateRequired(propertyIds, 'property_ids'); validateDateRange(params.start_date, params.end_date); return this.request({ method: 'GET', url: '/availabilities', params: { property_ids: propertyIds, start_date: params.start_date, end_date: params.end_date, }, }); } /** * Update property availabilities * @see https://docs.hostex.io/reference/update-availabilities */ async updateAvailabilities(params: UpdateAvailabilitiesParams): Promise { validateNonEmptyArray(params.property_ids, 'property_ids'); validateRequired(params.available, 'available'); if (params.start_date && params.end_date) { validateDateRange(params.start_date, params.end_date); } else if (!params.dates || params.dates.length === 0) { throw new Error('Either start_date+end_date or dates must be provided'); } const response = await this.axios.post('/availabilities', params); return response.data; } // ===================== // Listing Calendar API // ===================== /** * Query listing calendars * @see https://docs.hostex.io/reference/query-listing-calendars */ async getListingCalendars(params: CalendarQueryParams): Promise { validateRequired(params.start_date, 'start_date'); validateRequired(params.end_date, 'end_date'); validateNonEmptyArray(params.listings, 'listings'); validateDateRange(params.start_date, params.end_date); return this.request({ method: 'POST', url: '/listings/calendar', data: params, }); } /** * Update listing inventories * @see https://docs.hostex.io/reference/update-listing-inventories */ async updateListingInventories( params: UpdateListingInventoriesParams ): Promise { validateRequired(params.channel_type, 'channel_type'); validateRequired(params.listing_id, 'listing_id'); validateNonEmptyArray(params.inventories, 'inventories'); const response = await this.axios.post( '/listings/inventories', params ); return response.data; } /** * Update listing prices * @see https://docs.hostex.io/reference/update-listing-prices */ async updateListingPrices(params: UpdateListingPricesParams): Promise { validateRequired(params.channel_type, 'channel_type'); validateRequired(params.listing_id, 'listing_id'); validateNonEmptyArray(params.prices, 'prices'); const response = await this.axios.post('/listings/prices', params); return response.data; } /** * Update listing restrictions * @see https://docs.hostex.io/reference/update-listing-restrictions */ async updateListingRestrictions( params: UpdateListingRestrictionsParams ): Promise { validateRequired(params.channel_type, 'channel_type'); validateRequired(params.listing_id, 'listing_id'); validateNonEmptyArray(params.restrictions, 'restrictions'); const response = await this.axios.post( '/listings/restrictions', params ); return response.data; } // ===================== // Listings Catalogue API // ===================== /** * Query channel accounts * @see https://docs.hostex.io/reference/query-channel-accounts */ async getChannelAccounts(params?: ChannelAccountsQueryParams): Promise { return this.request({ method: 'GET', url: '/channel_accounts', params, }); } /** * Query channel listings * @see https://docs.hostex.io/reference/query-listings */ async getListings(params?: ListingsQueryParams): Promise { return this.request({ method: 'GET', url: '/listings', params, }); } /** * Update Airbnb listing price and rules * @see https://docs.hostex.io/reference/update-airbnb-listing-price-and-rules */ async updateAirbnbPriceAndRules(params: UpdateAirbnbPriceAndRulesParams): Promise { validateRequired(params.listing_id, 'listing_id'); validateRequired(params.settings, 'settings'); const response = await this.axios.post('/listings/airbnb/price_and_rules', params); return response.data; } /** * Update Vrbo listing price and rules * @see https://docs.hostex.io/reference/update-vrbo-listing-price-and-rules */ async updateVrboPriceAndRules(params: UpdateVrboPriceAndRulesParams): Promise { validateRequired(params.listing_id, 'listing_id'); validateRequired(params.settings, 'settings'); const response = await this.axios.post('/listings/vrbo/price_and_rules', params); return response.data; } /** * Query pricing ratios for a property or room type * @see https://docs.hostex.io/reference/query-pricing-ratios */ async getPricingRatios(params: PricingRatiosQueryParams): Promise { const hasProperty = params.property_id != null; const hasRoomType = params.room_type_id != null; if (hasProperty === hasRoomType) { throw new HostexApiError( 'Exactly one of property_id or room_type_id is required', 0, undefined, 400 ); } return this.request({ method: 'GET', url: '/pricing_ratios', params, }); } // ===================== // Calendar Share Links API // ===================== /** * Query calendar share links * @see https://docs.hostex.io/reference/query-calendar-share-links */ async getCalendarShareLinks(params?: CalendarShareLinksQueryParams): Promise { return this.request({ method: 'GET', url: '/calendar_share_links', params, }); } /** * Create a calendar share link * @see https://docs.hostex.io/reference/create-calendar-share-link */ async createCalendarShareLink( params: CreateCalendarShareLinkParams ): Promise> { validateRequired(params.scope, 'scope'); if (params.scope === 'partial') { // validateRequired narrows property_ids from number[] | undefined so the // strict-mode signature of validateNonEmptyArray (value: T[]) accepts it validateRequired(params.property_ids, 'property_ids'); validateNonEmptyArray(params.property_ids, 'property_ids'); } const response = await this.axios.post>( '/calendar_share_links', params ); return response.data; } /** * Delete a calendar share link * @see https://docs.hostex.io/reference/delete-calendar-share-link */ async deleteCalendarShareLink(id: number): Promise { validateRequired(id, 'id'); const response = await this.axios.delete(`/calendar_share_links/${id}`); return response.data; } // ===================== // Messages API // ===================== /** * Query conversations * @see https://docs.hostex.io/reference/query-conversations */ async getConversations(params?: ConversationsQueryParams): Promise { return this.request({ method: 'GET', url: '/conversations', params: { offset: params?.offset ?? 0, limit: params?.limit ?? 20, }, }); } /** * Get conversation details * @see https://docs.hostex.io/reference/get-conversation-details */ async getConversationDetails(conversationId: string): Promise { validateRequired(conversationId, 'conversationId'); return this.request({ method: 'GET', url: `/conversations/${conversationId}`, }); } /** * Send a message * @see https://docs.hostex.io/reference/send-message */ async sendMessage(params: SendMessageParams): Promise { validateRequired(params.conversation_id, 'conversation_id'); if (!params.message && !params.jpeg_base64) { throw new Error('Either message or jpeg_base64 must be provided'); } const response = await this.axios.post( `/conversations/${params.conversation_id}`, { message: params.message, jpeg_base64: params.jpeg_base64, } ); return response.data; } /** * Update the host-side private note on a conversation * @see https://docs.hostex.io/reference/update-conversation-note */ async updateConversationNote( params: UpdateConversationNoteParams ): Promise> { validateRequired(params.conversation_id, 'conversation_id'); const response = await this.axios.patch>( `/conversations/${params.conversation_id}/note`, { note: params.note } ); return response.data; } // ===================== // Reviews API // ===================== /** * Query reviews * @see https://docs.hostex.io/reference/query-reviews */ async getReviews(params?: ReviewsQueryParams): Promise { return this.request({ method: 'GET', url: '/reviews', params: { offset: params?.offset ?? 0, limit: params?.limit ?? 20, ...params, }, }); } /** * Create or update a review * @see https://docs.hostex.io/reference/create-review */ async createReview(params: CreateReviewParams): Promise { validateRequired(params.reservation_code, 'reservation_code'); const response = await this.axios.post( `/reviews/${params.reservation_code}`, { host_review_score: params.host_review_score, host_review_content: params.host_review_content, host_reply_content: params.host_reply_content, category_ratings: params.category_ratings, } ); return response.data; } // ===================== // Groups API // ===================== /** * Query groups * @see https://docs.hostex.io/reference/query-groups */ async getGroups(params?: GroupsQueryParams): Promise { return this.request({ method: 'GET', url: '/groups', params }); } /** * Create a group * @see https://docs.hostex.io/reference/create-group */ async createGroup(params: CreateGroupParams): Promise> { validateRequired(params.name, 'name'); const response = await this.axios.post>('/groups', params); return response.data; } /** * Update a group * @see https://docs.hostex.io/reference/update-group */ async updateGroup(params: UpdateGroupParams): Promise { validateRequired(params.id, 'id'); const { id, ...body } = params; const response = await this.axios.patch(`/groups/${id}`, body); return response.data; } /** * Delete a group * @see https://docs.hostex.io/reference/delete-group */ async deleteGroup(id: number): Promise { validateRequired(id, 'id'); const response = await this.axios.delete(`/groups/${id}`); return response.data; } // ===================== // Tags API (property / room type tags) // ===================== /** * Query tags * @see https://docs.hostex.io/reference/query-tags */ async getTags(params?: TagsQueryParams): Promise { return this.request({ method: 'GET', url: '/tags', params }); } /** * Create a tag * @see https://docs.hostex.io/reference/create-tag */ async createTag(params: CreateTagParams): Promise> { validateRequired(params.name, 'name'); const response = await this.axios.post>('/tags', params); return response.data; } /** * Update a tag * @see https://docs.hostex.io/reference/update-tag */ async updateTag(params: UpdateTagParams): Promise { validateRequired(params.id, 'id'); const { id, ...body } = params; const response = await this.axios.patch(`/tags/${id}`, body); return response.data; } /** * Delete a tag * @see https://docs.hostex.io/reference/delete-tag */ async deleteTag(id: number): Promise { validateRequired(id, 'id'); const response = await this.axios.delete(`/tags/${id}`); return response.data; } // ===================== // Reservation Tags API (tag dictionary for stays) // ===================== /** * Query reservation tags * @see https://docs.hostex.io/reference/query-reservation-tags */ async getReservationTags(params?: ReservationTagsQueryParams): Promise { return this.request({ method: 'GET', url: '/reservation_tags', params }); } /** * Create a reservation tag * @see https://docs.hostex.io/reference/create-reservation-tag */ async createReservationTag( params: CreateReservationTagParams ): Promise> { validateRequired(params.tag_name, 'tag_name'); const response = await this.axios.post>( '/reservation_tags', params ); return response.data; } /** * Delete a reservation tag * @see https://docs.hostex.io/reference/delete-reservation-tag */ async deleteReservationTag(id: number): Promise { validateRequired(id, 'id'); const response = await this.axios.delete(`/reservation_tags/${id}`); return response.data; } // ===================== // Staffs API // ===================== /** * Query staffs * @see https://docs.hostex.io/reference/query-staffs */ async getStaffs(params?: StaffsQueryParams): Promise { return this.request({ method: 'GET', url: '/staffs', params }); } /** * Create a staff * @see https://docs.hostex.io/reference/create-staff */ async createStaff(params: CreateStaffParams): Promise> { validateRequired(params.name, 'name'); validateRequired(params.mobile, 'mobile'); const response = await this.axios.post>('/staffs', params); return response.data; } /** * Update a staff * @see https://docs.hostex.io/reference/update-staff */ async updateStaff(params: UpdateStaffParams): Promise { validateRequired(params.id, 'id'); const { id, ...body } = params; const response = await this.axios.patch(`/staffs/${id}`, body); return response.data; } /** * Delete a staff * @see https://docs.hostex.io/reference/delete-staff */ async deleteStaff(id: number): Promise { validateRequired(id, 'id'); const response = await this.axios.delete(`/staffs/${id}`); return response.data; } // ===================== // Tasks API // ===================== /** * Query tasks * @see https://docs.hostex.io/reference/query-tasks */ async getTasks(params?: TasksQueryParams): Promise { return this.request({ method: 'GET', url: '/tasks', params }); } /** * Create a task * @see https://docs.hostex.io/reference/create-task */ async createTask(params: CreateTaskParams): Promise> { validateRequired(params.type, 'type'); validateRequired(params.expected_date, 'expected_date'); validateRequired(params.currency, 'currency'); const response = await this.axios.post>('/tasks', params); return response.data; } /** * Update a task * @see https://docs.hostex.io/reference/update-task */ async updateTask(params: UpdateTaskParams): Promise { validateRequired(params.id, 'id'); const { id, ...body } = params; const response = await this.axios.patch(`/tasks/${id}`, body); return response.data; } /** * Delete a task * @see https://docs.hostex.io/reference/delete-task */ async deleteTask(id: number): Promise { validateRequired(id, 'id'); const response = await this.axios.delete(`/tasks/${id}`); return response.data; } // ===================== // Incomes & Expenses API // ===================== /** * Query income items * @see https://docs.hostex.io/reference/query-income-items */ async getIncomeItems(): Promise { return this.request({ method: 'GET', url: '/income_items' }); } /** * Query expense items * @see https://docs.hostex.io/reference/query-expense-items */ async getExpenseItems(): Promise { return this.request({ method: 'GET', url: '/expense_items' }); } /** * Query expense methods * @see https://docs.hostex.io/reference/query-expense-methods */ async getExpenseMethods(): Promise { return this.request({ method: 'GET', url: '/expense_methods' }); } /** * Query incomes & expenses * @see https://docs.hostex.io/reference/query-transactions */ async getTransactions(params?: TransactionsQueryParams): Promise { return this.request({ method: 'GET', url: '/transactions', params }); } /** * Create a transaction (income or expense entry) * @see https://docs.hostex.io/reference/create-transaction */ async createTransaction(params: CreateTransactionParams): Promise> { validateRequired(params.direction, 'direction'); validateRequired(params.amount, 'amount'); validateRequired(params.item_id, 'item_id'); validateRequired(params.payment_method_id, 'payment_method_id'); if (!params.stay_code) { validateRequired(params.currency, 'currency'); } const response = await this.axios.post>('/transactions', params); return response.data; } /** * Update a transaction * @see https://docs.hostex.io/reference/update-transaction */ async updateTransaction(params: UpdateTransactionParams): Promise { validateRequired(params.id, 'id'); const { id, ...body } = params; const response = await this.axios.patch(`/transactions/${id}`, body); return response.data; } /** * Delete a transaction * @see https://docs.hostex.io/reference/delete-transaction */ async deleteTransaction(id: number): Promise { validateRequired(id, 'id'); const response = await this.axios.delete(`/transactions/${id}`); return response.data; } // ===================== // Knowledge Bases API // ===================== /** * Query knowledge bases * @see https://docs.hostex.io/reference/query-knowledge-bases */ async getKnowledgeBases(params?: KnowledgeBasesQueryParams): Promise { return this.request({ method: 'GET', url: '/knowledge_bases', params }); } /** * Get knowledge base detail * @see https://docs.hostex.io/reference/get-knowledge-base-detail */ async getKnowledgeBaseDetail(id: number): Promise { validateRequired(id, 'id'); return this.request({ method: 'GET', url: `/knowledge_bases/${id}` }); } /** * Create a knowledge base entry * @see https://docs.hostex.io/reference/create-knowledge-base */ async createKnowledgeBase(params: CreateKnowledgeBaseParams): Promise { validateRequired(params.scope_property, 'scope_property'); validateRequired(params.scope_channel, 'scope_channel'); validateNonEmptyArray(params.contents, 'contents'); if (params.is_enable == null) { throw new Error('is_enable is required'); } const response = await this.axios.post('/knowledge_bases', params); return response.data; } /** * Update a knowledge base entry * @see https://docs.hostex.io/reference/update-knowledge-base */ async updateKnowledgeBase(params: UpdateKnowledgeBaseParams): Promise { validateRequired(params.id, 'id'); validateRequired(params.scope_property, 'scope_property'); validateRequired(params.scope_channel, 'scope_channel'); validateNonEmptyArray(params.contents, 'contents'); if (params.is_enable == null) { throw new Error('is_enable is required'); } const { id, ...body } = params; const response = await this.axios.patch(`/knowledge_bases/${id}`, body); return response.data; } /** * Delete a knowledge base entry * @see https://docs.hostex.io/reference/delete-knowledge-base */ async deleteKnowledgeBase(id: number): Promise { validateRequired(id, 'id'); const response = await this.axios.delete(`/knowledge_bases/${id}`); return response.data; } // ===================== // Automation Actions API // ===================== /** * Query upcoming automation actions * @see https://docs.hostex.io/reference/query-automation-actions */ async getAutomationActions(params: AutomationActionsQueryParams): Promise { validateRequired(params.type, 'type'); return this.request({ method: 'GET', url: '/automation/actions', params, }); } /** * Delete an upcoming automation action * @see https://docs.hostex.io/reference/delete-automation-action */ async deleteAutomationAction(planId: number): Promise { validateRequired(planId, 'planId'); const response = await this.axios.delete(`/automation/actions/${planId}`); return response.data; } /** * Execute an automation action immediately * @see https://docs.hostex.io/reference/execute-automation-action */ async executeAutomationAction(planId: number): Promise { validateRequired(planId, 'planId'); const response = await this.axios.post(`/automation/actions/${planId}/execute`); return response.data; } // ===================== // Webhooks API // ===================== /** * Query webhooks * @see https://docs.hostex.io/reference/query-webhooks */ async getWebhooks(): Promise { return this.request({ method: 'GET', url: '/webhooks', }); } /** * Create a webhook * @see https://docs.hostex.io/reference/create-webhook */ async createWebhook(params: CreateWebhookParams): Promise { validateRequired(params.url, 'url'); validateUrl(params.url, 'url'); const response = await this.axios.post('/webhooks', params); return response.data; } /** * Update a webhook * @see https://docs.hostex.io/reference/update-webhook */ async updateWebhook(params: UpdateWebhookParams): Promise { validateRequired(params.id, 'id'); if (params.url !== undefined) { validateUrl(params.url, 'url'); } const { id, ...body } = params; const response = await this.axios.patch(`/webhooks/${id}`, body); return response.data; } /** * Delete a webhook * @see https://docs.hostex.io/reference/delete-webhook */ async deleteWebhook(params: DeleteWebhookParams): Promise { validateRequired(params.id, 'id'); const response = await this.axios.delete(`/webhooks/${params.id}`); return response.data; } // ===================== // OAuth API // ===================== /** * Obtain or refresh an access token * @see https://docs.hostex.io/reference/obtain-token */ async obtainToken(params: OAuthTokenParams): Promise { validateRequired(params.client_id, 'client_id'); validateRequired(params.client_secret, 'client_secret'); validateRequired(params.grant_type, 'grant_type'); if (params.grant_type === 'authorization_code') { validateRequired(params.code, 'code'); } else if (params.grant_type === 'refresh_token') { validateRequired(params.refresh_token, 'refresh_token'); } return this.request({ method: 'POST', url: '/oauth/authorizations', data: params, }); } /** * Revoke a token * @see https://docs.hostex.io/reference/revoke-token */ async revokeToken(params: RevokeTokenParams): Promise { validateRequired(params.client_id, 'client_id'); validateRequired(params.client_secret, 'client_secret'); validateRequired(params.token, 'token'); const response = await this.axios.post('/oauth/revoke', params); return response.data; } }