Для разработчиков

Swift

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

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

Установка

Добавьте пакет и его продукт PachcaSDK в Package.swift:

dependencies: [    .package(url: "https://github.com/pachca/openapi", from: "1.8.0")],targets: [    .target(name: "MyApp", dependencies: [        .product(name: "PachcaSDK", package: "openapi")    ])]

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

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

Клиенту нужен токен бота или персональный токен. Какой выбрать и кто может его выпустить, описано в разделе Какой токен выбрать.

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, inviterId: Int?, 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")
ПараметрТипПо умолчаниюОписание
tokenString—Bearer-токен для авторизации
baseURLStringhttps://api.pachca.com/api/shared/v1Базовый URL API

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

Все методы

МетодМетод API
client.oauth.revokeToken()POSTОтзыв токена
client.oauth.exchangeToken()POSTОбмен кода на токен
client.oauth.getTokenInfo()GETИнформация о токене
client.chats.createChat()POSTНовый чат
client.chats.requestExport()POSTЭкспорт сообщений
client.chats.listChats()GETСписок чатов
client.chats.downloadExport()GETСкачать архив экспорта
client.chats.getChat()GETИнформация о чате
client.chats.listCompanyChats()GETСписок чатов пространства
client.chats.updateChat()PUTРедактирование чата
client.chats.archiveChat()PUTАрхивация чата
client.chats.updateChatAvatar()PUTЗагрузка аватара чата
client.chats.unarchiveChat()PUTРазархивация чата
client.chats.markChatUnread()PUTОтметка чата непрочитанным
client.chats.deleteChatAvatar()DELETEУдаление аватара чата
client.profile.getProfile()GETСвой профиль
client.profile.getStatus()GETСвой статус
client.profile.updateProfileAvatar()PUTЗагрузка своего аватара
client.profile.updateStatus()PUTНовый свой статус
client.profile.deleteProfileAvatar()DELETEУдаление своего аватара
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.updateUserAvatar()PUTЗагрузка аватара сотрудника
client.users.updateUserStatus()PUTНовый статус сотрудника
client.users.deleteUser()DELETEУдаление сотрудника
client.users.deleteUserAvatar()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.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.createStandaloneThread()POSTНовый самостоятельный тред
client.threads.listThreads()GETСписок тредов
client.threads.getThread()GETИнформация о треде
client.messages.createMessage()POSTНовое сообщение
client.messages.unfurl()POSTUnfurl (разворачивание ссылок)
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.importantMessages.addMessageToImportant()POSTДобавление сообщения в важные
client.importantMessages.listImportantMessages()GETСписок важных сообщений
client.importantMessages.removeMessageFromImportant()DELETEУдаление сообщения из важных
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.drafts.createDraft()POSTНовый черновик
client.drafts.listDrafts()GETСписок черновиков
client.drafts.getDraft()GETИнформация о черновике
client.drafts.updateDraft()PUTРедактирование черновика
client.drafts.deleteDraft()DELETEУдаление черновика
client.views.openView()POSTОткрытие представления
client.views.submitViewResponse()POSTОтвет на отправку формы
client.bots.selfRecreateBotToken()POSTРотация собственного токена бота
client.bots.createBot()POSTНовый бот
client.bots.recreateBotToken()POSTРотация токена бота
client.bots.rotateBotClientSecret()POSTРотация секрета клиента
client.bots.createBotToken()POSTНовый токен бота
client.bots.reissueBotToken()POSTПеревыпуск токена бота
client.bots.listBots()GETСписок ботов
client.bots.getBot()GETИнформация о боте
client.bots.getBotScopes()GETКаталог прав бота
client.bots.listBotTokens()GETСписок токенов бота
client.bots.listCompanyBots()GETСписок ботов пространства
client.bots.getWebhookEvents()GETИстория событий
client.bots.getWebhookEventsStream()GETПоток событий
client.bots.selfUpdateBotWebhook()PUTСаморегистрация вебхука бота
client.bots.updateBot()PUTРедактирование бота
client.bots.updateBotAvatar()PUTЗагрузка аватара бота
client.bots.updateBotToken()PUTИзменение токена бота
client.bots.deleteBot()DELETEУдаление бота
client.bots.deleteBotAvatar()DELETEУдаление аватара бота
client.bots.deleteBotToken()DELETEУдаление токена бота
client.bots.deleteWebhookEvent()DELETEУдаление события истории
client.security.getAuditEvents()GETЖурнал аудита событий
client.customProperties.listProperties()GETСписок дополнительных полей
client.files.uploadFile()POSTЗагрузка файла
client.files.getUploadParams()POSTПолучение подписи, ключа и других параметров
client.files.downloadFile()GETСкачивание файла

Запросы

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

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

import PachcaSDK // Список чатовlet response = try await client.chats.listChats(sort: .id, order: .desc, availability: .isMember, archived: true, 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 request = ChatCreateRequest(    chat: ChatCreateRequestChat(        name: "🤿 aqua",        memberIds: [123],        groupTagIds: [123],        channel: true,        `public`: false    ))let response = try await client.chats.createChat(request: request)// → CreateChatResponse(data: Chat(id: Int, name: String, createdAt: String, ownerId: Int, memberIds: [Int], groupTagIds: [Int], channel: Bool, archived: Bool, personal: Bool, `public`: Bool, lastMessageAt: String, meetRoomUrl: String, avatarUrl: String?), meta: CreateChatResponseMeta(notFoundMemberIds: [Int], notFoundGroupTagIds: [Int]))

Простой вызов по 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, archived: Bool, personal: Bool, `public`: Bool, lastMessageAt: String, meetRoomUrl: String, avatarUrl: String?)

Значение null

Необязательное поле, которое не задано, в запрос не попадает, поэтому метод обновления меняет только то, что вы передали. Некоторые поля API принимают null: например, null в oauth_client выключает у бота авторизацию от имени сотрудника, а в webhook.kind удаляет входящий вебхук. Чтобы отправить null, назовите поле в nullFields той же модели запроса так, как оно называется в Swift:

let request = BotUpdateRequest(    webhook: BotUpdateRequestWebhook(nullFields: ["kind"]))

nullFields есть у моделей запроса с такими полями, а в его описании перечислены имена, которые в нём можно указать. С другим именем SDK выбрасывает EncodingError и запрос не отправляет.

Пагинация

Поля пагинации называются как в API, но в camelCase: meta.paginate.nextPage, prevPage, hasNext и hasPrev, а у методов поиска ещё meta.total. Какие поля приходят у каждого метода и как понять, что страницы закончились, описано в разделе Пагинация.

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

var cursor: String? = nilvar hasNext = truewhile hasNext {    let response = try await client.chats.listChats(limit: 50, cursor: cursor)    if response.data.isEmpty { break }    for chat in response.data {        print(chat.id, chat.name)    }    cursor = response.meta.paginate.nextPage    hasNext = response.meta.paginate.hasNext ?? true}

В модели поле hasNext имеет тип Bool?. Если оно nil, цикл идёт дальше и останавливается на пустой странице. Так же обходят страницы методы *All().

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

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

// Все чаты одним массивомlet chats = try await client.chats.listChatsAll()print("Всего: \(chats.count)")

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

МетодВозвращает
security.getAuditEventsAll()[AuditEvent]
bots.listBotsAll()[BotResponse]
bots.listBotTokensAll()[BotAccessToken]
bots.listCompanyBotsAll()[CompanyBotResponse]
bots.getWebhookEventsAll()[WebhookEvent]
chats.listChatsAll()[Chat]
chats.listCompanyChatsAll()[Chat]
drafts.listDraftsAll()[Draft]
groupTags.listTagsAll()[GroupTag]
groupTags.getTagUsersAll()[User]
importantMessages.listImportantMessagesAll()[Message]
members.listMembersAll()[User]
messages.listChatMessagesAll()[Message]
reactions.listReactionsAll()[Reaction]
search.searchChatsAll()[Chat]
search.searchMessagesAll()[Message]
search.searchUsersAll()[User]
tasks.listTasksAll()[Task]
threads.listThreadsAll()[Thread]
users.listUsersAll()[User]

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

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

ApiError

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

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

Поля ApiErrorItem:

ПолеТипОписание
keyStringПоле, вызвавшее ошибку
valueAnyCodable?Переданное значение
messageStringТекст ошибки
codeValidationErrorCodeКод валидации
payload[String: AnyCodable]?Дополнительные данные

Код статуса ответа — в поле statusCode. Если ответ с ошибкой пришёл без тела или не в JSON, SDK тоже выбрасывает ApiError: с кодом статуса и пустым errors. Так бывает с ответом 404 без тела от метода GETПоток событий, когда история событий выключена, и со страницей прокси при 502.

OAuthError

Возникает при ошибке авторизации: 401, а также 403 с кодом insufficient_scope или role_forbidden и отказе при обмене кода на токен. Код — в поле error, список кодов — в разделе Ошибки авторизации:

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

Поле error — перечисление OAuthErrorCode: его сравнивают со случаем, например error.error == .insufficientScope, или со строковым литералом, а строку кода даёт rawValue.

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

Паузы между попытками, к каждой SDK добавляет случайную величину до 25%:

  • 429 с заголовком Retry-After: срок из заголовка
  • 429 без заголовка: 1 сек, 2 сек, 4 сек
  • 5xx: 1 сек, 2 сек, 3 сек

Сетевую ошибку без ответа сервера SDK не повторяет: ошибка URLSession приходит сразу. Паузы идут через _Concurrency.Task.sleep(nanoseconds:) и поток не блокируют.

Общие правила повторов — в разделе Повторные запросы.

Типы

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

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

Перечисление в ответе принимает и значение, которого SDK ещё не знает: оно приходит случаем .unknown(String), а rawValue возвращает исходную строку. Поэтому switch по такому перечислению включает case .unknown или default. Незнакомую строку и init?(rawValue:) превращает в случай .unknown: проверить, знает ли SDK значение, помогает allCases.contains(value).

Все перечисления SDK: AuditEventKey, AuditLoginGrantDenyReason, AuditLoginGrantDevicePlatform, BotCanEdit, BotEventName, BotTemplateEngine, BotTriggerOn, BotWebhookKind, BotWhoCanAdd, ChatActivity, ChatAvailability, ChatMemberRole, ChatMemberRoleFilter, ChatSortField, ChatSubtype, CustomPropertyDataType, DraftRepetitionInterval, DraftType, FileTarget, FileType, InviteStatus, MemberEventType, MessageEntityType, MessageSearchSort, MessageSortField, OAuthErrorCode, OAuthScope, OAuthTokenTypeHint, ReactionEventType, SearchEntityType, SearchSortOrder, SortOrder, TaskKind, TaskStatus, UserCreateRole, UserEventType, UserRole, UserRoleInput, ValidationErrorCode, VideoCallEventType, VoiceTranscriptStatus и WebhookEventType.

Имена Task и Thread

В SDK есть типы Task и Thread. В файле с import PachcaSDK имя Task означает напоминание Пачки, поэтому задачу Swift Concurrency записывайте полностью: _Concurrency.Task. Имя Thread рядом с import Foundation неоднозначно, его записывайте как PachcaSDK.Thread или Foundation.Thread:

import Foundationimport PachcaSDK _Concurrency.Task {    let threads: [PachcaSDK.Thread] = try await client.threads.listThreadsAll()    print(threads.count)}

Тело вебхука

Тела событий вебхука описаны структурами MessageWebhookPayload, ReactionWebhookPayload, ButtonWebhookPayload, ViewSubmitWebhookPayload, ChatMemberWebhookPayload, CompanyMemberWebhookPayload, LinkSharedWebhookPayload и VideoCallWebhookPayload. Перечисление WebhookPayloadUnion объединяет их: при разборе JSON оно выбирает случай по полям type и event.

import Foundationimport PachcaSDK // body — тело запроса, Datalet payload = try JSONDecoder().decode(WebhookPayloadUnion.self, from: body)if case .buttonWebhookPayload(let button) = payload {    print(button.messageId, button.data)}

Подпись проверяйте по исходному телу до разбора, как в разделе Безопасность. Те же типы отдают методы pollWebhookEvents и pollWebhookPayloads, которые забирают события из истории: пример — в разделе Пример поллинга.

Платформы

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

Примеры

Отправка сообщения:

import PachcaSDK let client = PachcaClient(token: "YOUR_TOKEN") let request = MessageCreateRequest(message: MessageCreateRequestMessage(entityId: 334, content: "Вчера мы продали 756 футболок (что на 10% больше, чем в прошлое воскресенье)"))let response = try await client.messages.createMessage(request: request)// → 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?, durationMs: Int?)], voiceContent: VoiceContent(durationMs: Int, waveform: String, transcript: String?, transcriptStatus: VoiceTranscriptStatus?)?, buttons: [[Button(text: String, url: String?, data: String?)]]?, thread: MessageThread(id: Int64, chatId: Int64)?, 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?, important: Bool)

Список сотрудников:

import PachcaSDK let client = PachcaClient(token: "YOUR_TOKEN") let response = try await client.users.listUsers(query: "Олег", limit: 1, cursor: "eyJpZCI6MTAsImRpciI6ImFzYyJ9")// → ListUsersResponse(data: [User], meta: PaginationMeta)

Создание напоминания:

import PachcaSDK let client = PachcaClient(token: "YOUR_TOKEN") let request = 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(request: request)// → 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?)])