Swift

Типизированный клиент для Pachca API на Swift. Построен на URLSession с async throws, Codable-моделями и встроенным retry. Требуется Swift 5.9+, macOS 13+ или iOS 16+.

Быстрый старт

Установка

Добавьте пакет в Package.swift:

dependencies: [    .package(url: "https://github.com/pachca/openapi", from: "1.0.1")]

Или через Xcode: File → Add Package Dependencies → https://github.com/pachca/openapi.

Создание клиента

Получите API-токен в интерфейсе Пачки: Настройки → Автоматизации → API (подробнее — Авторизация).

import PachcaSDK let client = PachcaClient(token: "YOUR_TOKEN")

Первый запрос

import PachcaSDK // Получение профиляlet response = try await client.profile.getProfile()// → User(id: Int, firstName: String, lastName: String, nickname: String, email: String, phoneNumber: String, department: String, title: String, role: UserRole, suspended: Bool, inviteStatus: InviteStatus, listTags: [String], customProperties: [CustomProperty(id: Int, name: String, dataType: CustomPropertyDataType, value: String)], userStatus: UserStatus(emoji: String, title: String, expiresAt: String?, isAway: Bool, awayMessage: UserStatusAwayMessage(text: String)?)?, bot: Bool, sso: Bool, createdAt: String, lastActivityAt: String, timeZone: String, imageUrl: String?)

Инициализация

import PachcaSDK // Стандартное подключениеlet client = PachcaClient(token: "YOUR_TOKEN") // С кастомным базовым URLlet client = PachcaClient(token: "YOUR_TOKEN", baseURL: "https://custom-api.example.com/api/shared/v1")
ПараметрТипПо умолчаниюОписание
tokenStringBearer-токен для авторизации
baseURLStringhttps://api.pachca.com/api/shared/v1Базовый URL API

Клиент — value type (struct), использует URLSession.shared. Явного закрытия не требует.

Все методы

МетодМетод API
client.common.requestExport()POSTЭкспорт сообщений
client.common.uploadFile()POSTЗагрузка файла
client.common.getUploadParams()POSTПолучение подписи, ключа и других параметров
client.common.downloadExport()GETСкачать архив экспорта
client.common.listProperties()GETСписок дополнительных полей
client.profile.getTokenInfo()GETИнформация о токене
client.profile.getProfile()GETИнформация о профиле
client.profile.getStatus()GETТекущий статус
client.profile.updateStatus()PUTНовый статус
client.profile.deleteStatus()DELETEУдаление статуса
client.users.createUser()POSTСоздать сотрудника
client.users.listUsers()GETСписок сотрудников
client.users.getUser()GETИнформация о сотруднике
client.users.getUserStatus()GETСтатус сотрудника
client.users.updateUser()PUTРедактирование сотрудника
client.users.updateUserStatus()PUTНовый статус сотрудника
client.users.deleteUser()DELETEУдаление сотрудника
client.users.deleteUserStatus()DELETEУдаление статуса сотрудника
client.groupTags.createTag()POSTНовый тег
client.groupTags.listTags()GETСписок тегов сотрудников
client.groupTags.getTag()GETИнформация о теге
client.groupTags.getTagUsers()GETСписок сотрудников тега
client.groupTags.updateTag()PUTРедактирование тега
client.groupTags.deleteTag()DELETEУдаление тега
client.chats.createChat()POSTНовый чат
client.chats.listChats()GETСписок чатов
client.chats.getChat()GETИнформация о чате
client.chats.updateChat()PUTОбновление чата
client.chats.archiveChat()PUTАрхивация чата
client.chats.unarchiveChat()PUTРазархивация чата
client.members.addTags()POSTДобавление тегов
client.members.addMembers()POSTДобавление пользователей
client.members.listMembers()GETСписок участников чата
client.members.updateMemberRole()PUTРедактирование роли
client.members.removeTag()DELETEИсключение тега
client.members.leaveChat()DELETEВыход из беседы или канала
client.members.removeMember()DELETEИсключение пользователя
client.threads.createThread()POSTНовый тред
client.threads.getThread()GETИнформация о треде
client.messages.createMessage()POSTНовое сообщение
client.messages.pinMessage()POSTЗакрепление сообщения
client.messages.listChatMessages()GETСписок сообщений чата
client.messages.getMessage()GETИнформация о сообщении
client.messages.updateMessage()PUTРедактирование сообщения
client.messages.deleteMessage()DELETEУдаление сообщения
client.messages.unpinMessage()DELETEОткрепление сообщения
client.readMembers.listReadMembers()GETСписок прочитавших сообщение
client.reactions.addReaction()POSTДобавление реакции
client.reactions.listReactions()GETСписок реакций
client.reactions.removeReaction()DELETEУдаление реакции
client.linkPreviews.createLinkPreviews()POSTUnfurl (разворачивание ссылок)
client.search.searchChats()GETПоиск чатов
client.search.searchMessages()GETПоиск сообщений
client.search.searchUsers()GETПоиск сотрудников
client.tasks.createTask()POSTНовое напоминание
client.tasks.listTasks()GETСписок напоминаний
client.tasks.getTask()GETИнформация о напоминании
client.tasks.updateTask()PUTРедактирование напоминания
client.tasks.deleteTask()DELETEУдаление напоминания
client.views.openView()POSTОткрытие представления
client.bots.getWebhookEvents()GETИстория событий
client.bots.updateBot()PUTРедактирование бота
client.bots.deleteWebhookEvent()DELETEУдаление события
client.security.getAuditEvents()GETЖурнал аудита событий

Запросы

Все методы — async throws.

GET с параметрами:

import PachcaSDK // Список чатовlet response = try await client.chats.listChats(sortId: .desc, availability: .isMember, lastMessageAtAfter: "2025-01-01T00:00:00.000Z", lastMessageAtBefore: "2025-02-01T00:00:00.000Z", personal: false, limit: 1, cursor: "eyJpZCI6MTAsImRpciI6ImFzYyJ9")// → ListChatsResponse(data: [Chat], meta: PaginationMeta?)

POST с телом запроса:

import PachcaSDK // Создание чатаlet body = ChatCreateRequest(    chat: ChatCreateRequestChat(        name: "🤿 aqua",        memberIds: [123],        groupTagIds: [123],        channel: true,        `public`: false    ))let response = try await client.chats.createChat(body: body)// → Chat(id: Int, name: String, createdAt: String, ownerId: Int, memberIds: [Int], groupTagIds: [Int], channel: Bool, personal: Bool, `public`: Bool, lastMessageAt: String, meetRoomUrl: String)

Простой вызов по ID:

import PachcaSDK // Получение чатаlet response = try await client.chats.getChat(id: 334)// → Chat(id: Int, name: String, createdAt: String, ownerId: Int, memberIds: [Int], groupTagIds: [Int], channel: Bool, personal: Bool, `public`: Bool, lastMessageAt: String, meetRoomUrl: String)

Пагинация

Методы, возвращающие списки, используют cursor-based пагинацию. Ответ содержит meta?.paginate?.nextPage — курсор для следующей страницы.

Ручная пагинация

var cursor: String? = nilrepeat {    let response = try await client.users.listUsers(limit: 50, cursor: cursor)    for user in response.data {        print("\(user.firstName) \(user.lastName)")    }    cursor = response.meta?.paginate?.nextPage} while cursor != nil

Автопагинация

Для каждого метода с пагинацией есть *All() вариант:

// Все пользователи одним массивомlet users = try await client.users.listUsersAll()print("Всего: \(users.count)")

Доступные методы автопагинации:

МетодВозвращает
security.getAuditEventsAll()[AuditEvent]
bots.getWebhookEventsAll()[WebhookEvent]
chats.listChatsAll()[Chat]
groupTags.listTagsAll()[GroupTag]
groupTags.getTagUsersAll()[User]
members.listMembersAll()[User]
messages.listChatMessagesAll()[Message]
reactions.listReactionsAll()[Reaction]
search.searchChatsAll()[Chat]
search.searchMessagesAll()[Message]
search.searchUsersAll()[User]
tasks.listTasksAll()[Task]
users.listUsersAll()[User]

Обработка ошибок

SDK выбрасывает два типа ошибок (реализуют протокол Error):

ApiError

Возникает при ошибках 400, 403, 404, 409, 410, 422:

do {    try await client.chats.createChat(request)} catch let error as ApiError {    for e in error.errors {        print("\(e.key): \(e.message)")  // "name: не может быть пустым"        print(e.code)                    // .blank    }}

Поля ApiErrorItem:

ПолеТипОписание
keyStringПоле, вызвавшее ошибку
valueString?Переданное значение
messageStringТекст ошибки
codeValidationErrorCodeКод валидации
payloadString?Дополнительные данные

OAuthError

Возникает при ошибке авторизации (401):

do {    try await client.profile.getProfile()} catch let error as OAuthError {    print(error.error)              // "Token not found"    print(error.errorDescription)   // описание ошибки}

Повторные запросы

SDK автоматически повторяет запрос при получении 429 Too Many Requests:

  • До 3 повторов на каждый запрос
  • Если сервер вернул заголовок Retry-After — ждёт указанное время
  • Иначе — экспоненциальный backoff: 1 сек, 2 сек, 4 сек
  • Ожидание через Task.sleep(nanoseconds:) — не блокирует поток

Типы

Все типы — Codable struct'ы:

import PachcaSDK // Моделиlet chat: Chatlet message: Messagelet user: User // Перечисленияlet key: AuditEventKey = .userLoginlet availability: ChatAvailability = .isOpenlet role: ChatMemberRole = .adminlet status: TaskStatus = .done // Ошибкиlet apiError: ApiErrorlet oauthError: OAuthError

Доступные перечисления: AuditEventKey, ChatAvailability, ChatMemberRole, ChatMemberRoleFilter, ChatSubtype, CustomPropertyDataType, FileType, InviteStatus, MemberEventType, MessageEntityType, OAuthScope, ReactionEventType, SearchEntityType, SearchSortOrder, SortOrder, TaskKind, TaskStatus, UserEventType, UserRole, ValidationErrorCode, WebhookEventType.

Платформы

ПлатформаМинимальная версия
macOS13.0+
iOS16.0+
Swift5.9+

Примеры

import PachcaSDK let client = PachcaClient(token: "YOUR_TOKEN") // Отправка сообщенияlet body = MessageCreateRequest(    message: MessageCreateRequestMessage(        entityType: .discussion,        entityId: 334,        content: "Вчера мы продали 756 футболок (что на 10% больше, чем в прошлое воскресенье)",        files: [MessageCreateRequestFile(            key: "attaches/files/93746/e354fd79-4f3e-4b5a-9c8d-1a2b3c4d5e6f/logo.png",            name: "logo.png",            fileType: .image,            size: 12345,            width: 800,            height: 600        )],        buttons: [[Button(            text: "Подробнее",            url: "https://example.com/details",            data: "awesome"        )]],        parentMessageId: 194270,        displayAvatarUrl: "https://example.com/avatar.png",        displayName: "Бот Поддержки",        skipInviteMentions: false,        linkPreview: false    ))let response = try await client.messages.createMessage(body: body)// → Message(id: Int, entityType: MessageEntityType, entityId: Int, chatId: Int, rootChatId: Int, content: String, userId: Int, createdAt: String, url: String, files: [File(id: Int, key: String, name: String, fileType: FileType, url: String, width: Int?, height: Int?)], buttons: [[Button(text: String, url: String?, data: String?)]]?, thread: Thread(id: Int64, chatId: Int64, messageId: Int64, messageChatId: Int64, updatedAt: String)?, forwarding: Forwarding(originalMessageId: Int, originalChatId: Int, authorId: Int, originalCreatedAt: String, originalThreadId: Int?, originalThreadMessageId: Int?, originalThreadParentChatId: Int?)?, parentMessageId: Int?, displayAvatarUrl: String?, displayName: String?, changedAt: String?, deletedAt: String?) // Список сотрудниковlet response = try await client.users.listUsers(query: "Олег", limit: 1, cursor: "eyJpZCI6MTAsImRpciI6ImFzYyJ9")// → ListUsersResponse(data: [User], meta: PaginationMeta?) // Создание задачиlet body = TaskCreateRequest(    task: TaskCreateRequestTask(        kind: .reminder,        content: "Забрать со склада 21 заказ",        dueAt: "2020-06-05T12:00:00.000+03:00",        priority: 2,        performerIds: [123],        chatId: 456,        allDay: false,        customProperties: [TaskCreateRequestCustomProperty(id: 78, value: "Синий склад")]    ))let response = try await client.tasks.createTask(body: body)// → Task(id: Int, kind: TaskKind, content: String, dueAt: String?, priority: Int, userId: Int, chatId: Int?, status: TaskStatus, createdAt: String, performerIds: [Int], allDay: Bool, customProperties: [CustomProperty(id: Int, name: String, dataType: CustomPropertyDataType, value: String)])