@adeli/sdk 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/uploads.ts"],"names":[],"mappings":";;;;AAYA,eAAsB,YAAA,CAAa,IAAA,EAAc,OAAA,GAAkD,EAAC,EAAkB;AACpH,EAAA,OAAO,UAAA,CAAW,IAAA,EAAM,OAAA,CAAQ,WAAA,KAAgB,MAAA,GAAY,SAAY,EAAE,IAAA,EAAM,OAAA,CAAQ,WAAA,EAAa,CAAA;AACvG;AAkBA,eAAsB,iBACpB,SAAA,EACA,WAAA,EACA,KAAA,EACA,OAAA,GAA+B,EAAC,EACJ;AAC5B,EAAA,IAAI,YAAY,MAAA,KAAW,KAAA,CAAM,QAAQ,MAAM,IAAI,UAAU,0CAA0C,CAAA;AACvG,EAAA,MAAM,IAAA,GAAO,IAAI,GAAA,CAAI,WAAA,CAAY,GAAA,CAAI,CAAC,UAAA,KAAe,CAAC,UAAA,CAAW,QAAA,EAAU,UAAU,CAAC,CAAC,CAAA;AACvF,EAAA,MAAM,WAAA,GAAc,QAAQ,WAAA,IAAe,CAAA;AAC3C,EAAA,IAAI,CAAC,MAAA,CAAO,aAAA,CAAc,WAAW,CAAA,IAAK,WAAA,GAAc,CAAA,IAAK,WAAA,GAAc,EAAA,EAAI,MAAM,IAAI,SAAA,CAAU,iDAAiD,CAAA;AACpJ,EAAA,MAAM,KAAA,GAAQ,MAAM,MAAA,CAAO,CAAC,KAAK,IAAA,KAAS,GAAA,GAAM,IAAA,CAAK,IAAA,EAAM,CAAC,CAAA;AAC5D,EAAA,IAAI,cAAA,GAAiB,CAAA;AACrB,EAAA,IAAI,cAAA,GAAiB,CAAA;AACrB,EAAA,MAAM,MAAA,GAAmB,IAAI,KAAA,CAAM,KAAA,CAAM,MAAM,CAAA;AAC/C,EAAA,IAAI,IAAA,GAAO,CAAA;AACX,EAAA,MAAM,SAAS,YAAY;AACzB,IAAA,OAAO,IAAA,GAAO,MAAM,MAAA,EAAQ;AAC1B,MAAA,MAAM,KAAA,GAAQ,IAAA,EAAA;AACd,MAAA,MAAM,UAAA,GAAa,IAAA,CAAK,GAAA,CAAI,MAAA,CAAO,KAAK,CAAC,CAAA;AACzC,MAAA,MAAM,IAAA,GAAO,MAAM,KAAK,CAAA;AACxB,MAAA,IAAI,CAAC,UAAA,IAAc,CAAC,MAAM,MAAM,IAAI,UAAU,0CAA0C,CAAA;AACxF,MAAA,MAAM,SAAA,CAAU,OAAO,UAAA,CAAW,GAAA,EAAK,WAAW,MAAA,EAAQ,UAAA,CAAW,OAAA,EAAS,IAAA,EAAM,OAAO,CAAA;AAC3F,MAAA,cAAA,IAAkB,IAAA,CAAK,IAAA;AACvB,MAAA,cAAA,IAAkB,CAAA;AAClB,MAAA,OAAA,CAAQ,UAAA,GAAa,EAAE,MAAA,EAAQ,cAAA,EAAgB,OAAO,cAAA,EAAgB,UAAA,EAAY,KAAA,CAAM,MAAA,EAAQ,CAAA;AAChG,MAAA,MAAA,CAAO,KAAK,IAAI,UAAA,CAAW,QAAA;AAAA,IAC7B;AAAA,EACF,CAAA;AACA,EAAA,MAAM,QAAQ,GAAA,CAAI,KAAA,CAAM,IAAA,CAAK,EAAE,QAAQ,IAAA,CAAK,GAAA,CAAI,WAAA,EAAa,KAAA,CAAM,MAAM,CAAA,EAAE,EAAG,MAAM,MAAA,EAAQ,CAAC,CAAA;AAC7F,EAAA,OAAO,MAAA;AACT;AAGA,eAAsB,YAAA,CAAa,IAAA,EAAc,OAAA,GAA6B,EAAC,EAAkB;AAC/F,EAAA,MAAM,IAAA,GAAO,MAAM,YAAA,CAAa,IAAA,EAAM,OAAO,CAAA;AAC7C,EAAA,OAAO,IAAI,IAAA,CAAK,CAAC,IAAI,CAAA,EAAG,OAAA,CAAQ,QAAQ,QAAA,CAAS,IAAI,CAAA,EAAG,OAAA,CAAQ,gBAAgB,MAAA,GAAY,MAAA,GAAY,EAAE,IAAA,EAAM,OAAA,CAAQ,aAAa,CAAA;AACvI","file":"uploads.js","sourcesContent":["import { basename } from 'node:path';\nimport { openAsBlob } from 'node:fs';\nimport type { RequestOptions } from './core/types.js';\nimport type { Transport } from './core/transport.js';\n\n/** Options for a file-backed upload. The bytes remain backed by the source file until consumed by fetch. */\nexport interface FileUploadOptions {\n readonly contentType?: string;\n readonly name?: string;\n}\n\n/** Open a local file as a native Blob without reading the media into memory. */\nexport async function blobFromPath(path: string, options: Pick<FileUploadOptions, 'contentType'> = {}): Promise<Blob> {\n return openAsBlob(path, options.contentType === undefined ? undefined : { type: options.contentType });\n}\n\nexport interface DirectUploadDescriptor {\n readonly clientId: string;\n readonly uploadId: string;\n readonly method: 'PUT';\n readonly url: string;\n readonly headers: Readonly<Record<string, string>>;\n readonly expiresAt: string;\n}\n\nexport interface DirectUploadProgress { readonly loaded: number; readonly total: number; readonly completedFiles: number; readonly totalFiles: number }\nexport interface DirectUploadOptions extends RequestOptions {\n readonly concurrency?: number;\n readonly onProgress?: (progress: DirectUploadProgress) => void;\n}\n\n/** PUT file/blob bodies with exact signed headers, bounded concurrency, cancellation, and stable result order. */\nexport async function putDirectUploads(\n transport: Transport,\n descriptors: readonly DirectUploadDescriptor[],\n files: readonly Blob[],\n options: DirectUploadOptions = {},\n): Promise<readonly string[]> {\n if (descriptors.length !== files.length) throw new TypeError('The media upload manifest was incomplete');\n const byId = new Map(descriptors.map((descriptor) => [descriptor.clientId, descriptor]));\n const concurrency = options.concurrency ?? 3;\n if (!Number.isSafeInteger(concurrency) || concurrency < 1 || concurrency > 10) throw new TypeError('concurrency must be an integer between 1 and 10');\n const total = files.reduce((sum, file) => sum + file.size, 0);\n let completedBytes = 0;\n let completedFiles = 0;\n const result: string[] = new Array(files.length);\n let next = 0;\n const worker = async () => {\n while (next < files.length) {\n const index = next++;\n const descriptor = byId.get(String(index));\n const file = files[index];\n if (!descriptor || !file) throw new TypeError('The media upload manifest was incomplete');\n await transport.upload(descriptor.url, descriptor.method, descriptor.headers, file, options);\n completedBytes += file.size;\n completedFiles += 1;\n options.onProgress?.({ loaded: completedBytes, total, completedFiles, totalFiles: files.length });\n result[index] = descriptor.uploadId;\n }\n };\n await Promise.all(Array.from({ length: Math.min(concurrency, files.length) }, () => worker()));\n return result;\n}\n\n/** Open a local file as a native File, retaining its filename and MIME type for direct upload. */\nexport async function fileFromPath(path: string, options: FileUploadOptions = {}): Promise<File> {\n const blob = await blobFromPath(path, options);\n return new File([blob], options.name ?? basename(path), options.contentType === undefined ? undefined : { type: options.contentType });\n}\n"]}
@@ -0,0 +1,72 @@
1
+ # API reference
2
+
3
+ Generated from `src/operations.ts` metadata, AST-extracted resource method declarations, and the checked-in verified public docs mapping `openapi/docs-links.json`; this file is not hand-maintained.
4
+
5
+ - Contract status: **candidate-unreleased**
6
+ - Candidate OpenAPI SHA-256: `e1a4cd95b91e14c2732275213493caebaa7ee3d83dffec27c001511eda32fc35`
7
+ - Public endpoint documentation: https://www.tryadeli.com/docs/
8
+
9
+ | Resource | Method signature | HTTP | Path | Endpoint docs |
10
+ | --- | --- | --- | --- | --- |
11
+ | Adeli | `getMe(args: { readonly query?: GetMeQuery } = {}, options: RequestOptions = {}): Promise<ApiResponse<Identity>>` | GET | `/me` | [api/me](https://www.tryadeli.com/docs/api/me) |
12
+ | profiles | `listProfiles(_args: EmptyArgs = {}, options: RequestOptions = {}): Promise<ApiResponse<ListProfilesResponse>>` | GET | `/profiles` | [api/profiles/list-profiles](https://www.tryadeli.com/docs/api/profiles/list-profiles) |
13
+ | profiles | `createProfile(args: { readonly body: CreateProfileBody }, options: RequestOptions & { readonly idempotencyKey?: string } = {}): Promise<ApiResponse<Profile>>` | POST | `/profiles` | [api/profiles/create-profile](https://www.tryadeli.com/docs/api/profiles/create-profile) |
14
+ | profiles | `getProfile(args: { readonly path: ProfilePath }, options: RequestOptions = {}): Promise<ApiResponse<Profile>>` | GET | `/profiles/{profileId}` | [api/profiles/get-profile](https://www.tryadeli.com/docs/api/profiles/get-profile) |
15
+ | profiles | `updateProfile(args: { readonly path: ProfilePath; readonly body: UpdateProfileBody }, options: RequestOptions = {}): Promise<ApiResponse<Profile>>` | PUT | `/profiles/{profileId}` | [api/profiles/update-profile](https://www.tryadeli.com/docs/api/profiles/update-profile) |
16
+ | profiles | `deleteProfile(args: { readonly path: ProfilePath }, options: RequestOptions = {}): Promise<ApiResponse<DeleteProfileResponse>>` | DELETE | `/profiles/{profileId}` | [api/profiles/delete-profile](https://www.tryadeli.com/docs/api/profiles/delete-profile) |
17
+ | profiles | `startConnection(args: { readonly path: ProfilePath; readonly body: StartConnectionBody }, options: RequestOptions = {}): Promise<ApiResponse<ConnectionSession>>` | POST | `/profiles/{profileId}/connect` | [api/connect/start-connection](https://www.tryadeli.com/docs/api/connect/start-connection) |
18
+ | profiles | `getConnection(args: { readonly path: ProfilePath & { readonly connectionSessionId: string } }, options: RequestOptions = {}): Promise<ApiResponse<ConnectionSession>>` | GET | `/profiles/{profileId}/connect/{connectionSessionId}` | [api/connect/get-connection-session](https://www.tryadeli.com/docs/api/connect/get-connection-session) |
19
+ | accounts | `listAccounts(args: { readonly query?: ListAccountsQuery } = {}, options: RequestOptions = {}): Promise<ApiResponse<AccountsAggregateResponse>>` | GET | `/accounts` | [api/accounts/list-accounts](https://www.tryadeli.com/docs/api/accounts/list-accounts) |
20
+ | accounts | `listProfileAccounts(args: { readonly path: { readonly profileId: string } }, options: RequestOptions = {}): Promise<ApiResponse<AccountsAggregateResponse>>` | GET | `/profiles/{profileId}/accounts` | [api/accounts/list-profile-accounts](https://www.tryadeli.com/docs/api/accounts/list-profile-accounts) |
21
+ | accounts | `getAccount(args: { readonly path: AccountPath }, options: RequestOptions = {}): Promise<ApiResponse<AccountDetailResponse>>` | GET | `/profiles/{profileId}/accounts/{accountId}` | [api/accounts/get-account](https://www.tryadeli.com/docs/api/accounts/get-account) |
22
+ | accounts | `updateAccount(args: { readonly path: AccountPath; readonly body: UpdateAccountBody }, options: RequestOptions = {}): Promise<ApiResponse<AccountUpdateResponse>>` | PATCH | `/profiles/{profileId}/accounts/{accountId}` | [api/accounts/update-x-account](https://www.tryadeli.com/docs/api/accounts/update-x-account) |
23
+ | accounts | `disconnectAccount(args: { readonly path: AccountPath }, options: RequestOptions = {}): Promise<ApiResponse<DisconnectAccountResponse>>` | DELETE | `/profiles/{profileId}/accounts/{accountId}` | [api/accounts/disconnect-account](https://www.tryadeli.com/docs/api/accounts/disconnect-account) |
24
+ | accounts | `refreshAccounts(args: { readonly path: { readonly profileId: string } }, options: RequestOptions = {}): Promise<ApiResponse<RefreshAccountsResponse>>` | POST | `/profiles/{profileId}/accounts/refresh` | [api/accounts/refresh-tokens](https://www.tryadeli.com/docs/api/accounts/refresh-tokens) |
25
+ | posts | `listPosts(args: { readonly query?: PostsQuery } = {}, options: RequestOptions = {}): Promise<ApiResponse<PostListResponse>>` | GET | `/posts` | [api/posts/list-posts](https://www.tryadeli.com/docs/api/posts/list-posts) |
26
+ | posts | `createMediaUploads(args: { readonly body: CreateMediaUploadsBody }, options: RequestOptions = {}): Promise<ApiResponse<CreateMediaUploadsResponse>>` | POST | `/media/uploads` | [api/posts/create-media-uploads](https://www.tryadeli.com/docs/api/posts/create-media-uploads) |
27
+ | posts | `createPost(args: { readonly body: CreatePostWithFiles \| TikTokMultipartBody }, options: DirectUploadOptions = {}): Promise<ApiResponse<SuccessBody<'createPost'>>>` | POST | `/posts` | [api/posts/publish-instagram](https://www.tryadeli.com/docs/api/posts/publish-instagram)<br>[api/posts/publish-facebook](https://www.tryadeli.com/docs/api/posts/publish-facebook)<br>[api/posts/publish-tiktok](https://www.tryadeli.com/docs/api/posts/publish-tiktok)<br>[api/posts/publish-youtube](https://www.tryadeli.com/docs/api/posts/publish-youtube)<br>[api/posts/publish-x](https://www.tryadeli.com/docs/api/posts/publish-x)<br>[api/posts/publish-bluesky](https://www.tryadeli.com/docs/api/posts/publish-bluesky) |
28
+ | posts | `uploadYouTubeVideo(args: { readonly body: YouTubeUploadBody \| YouTubeStagedUploadBody }, options: DirectUploadOptions = {}): Promise<ApiResponse<YouTubeUploadResponse>>` | POST | `/posts/youtube` | [api/posts/upload-youtube-video-file](https://www.tryadeli.com/docs/api/posts/upload-youtube-video-file) |
29
+ | posts | `deleteXPost(args: { readonly path: Path<'deleteXPost'>; readonly query: JsonQuery<'deleteXPost'> }, options: RequestOptions = {}): Promise<ApiResponse<DeletePostResponse>>` | DELETE | `/posts/x/{postId}` | [api/posts/delete-x-post](https://www.tryadeli.com/docs/api/posts/delete-x-post) |
30
+ | posts | `deleteBlueskyPost(args: { readonly path: Path<'deleteBlueskyPost'>; readonly query: JsonQuery<'deleteBlueskyPost'> }, options: RequestOptions = {}): Promise<ApiResponse<SuccessBody<'deleteBlueskyPost'>>>` | DELETE | `/posts/bluesky/{postId}` | [api/posts/delete-bluesky-post](https://www.tryadeli.com/docs/api/posts/delete-bluesky-post) |
31
+ | posts | `getInstagramPostStatus(args: { readonly query: JsonQuery<'getInstagramPostStatus'> }, options: RequestOptions = {}): Promise<ApiResponse<InstagramPostStatusResponse>>` | GET | `/posts/instagram/status` | [api/posts/get-instagram-publish-status](https://www.tryadeli.com/docs/api/posts/get-instagram-publish-status) |
32
+ | posts | `getFacebookPostStatus(args: { readonly query: JsonQuery<'getFacebookPostStatus'> }, options: RequestOptions = {}): Promise<ApiResponse<FacebookPostStatusResponse>>` | GET | `/posts/facebook/status` | [api/posts/get-facebook-video-status](https://www.tryadeli.com/docs/api/posts/get-facebook-video-status) |
33
+ | posts | `getTikTokPostStatus(args: { readonly query: JsonQuery<'getTikTokPostStatus'> }, options: RequestOptions = {}): Promise<ApiResponse<TikTokPostStatusResponse>>` | GET | `/posts/tiktok/status` | [api/posts/get-tiktok-publish-status](https://www.tryadeli.com/docs/api/posts/get-tiktok-publish-status) |
34
+ | posts | `getTikTokCreatorInfo(args: { readonly query: JsonQuery<'getTikTokCreatorInfo'> }, options: RequestOptions = {}): Promise<ApiResponse<TikTokCreatorInfoResponse>>` | GET | `/posts/tiktok/creator-info` | [api/posts/get-tiktok-creator-info](https://www.tryadeli.com/docs/api/posts/get-tiktok-creator-info) |
35
+ | posts | `getYouTubeVideoStatus(args: { readonly query: JsonQuery<'getYouTubeVideoStatus'> }, options: RequestOptions = {}): Promise<ApiResponse<YouTubeVideoStatusResponse>>` | GET | `/posts/youtube/status` | [api/posts/get-youtube-upload-status](https://www.tryadeli.com/docs/api/posts/get-youtube-upload-status) |
36
+ | comments | `listComments(args: { readonly query: CommentsQuery }, options: RequestOptions = {}): Promise<ApiResponse<CommentsResponse>>` | GET | `/comments` | [api/comments/list-comments](https://www.tryadeli.com/docs/api/comments/list-comments) |
37
+ | comments | `createComment(args: { readonly body: CreateCommentBody }, options: RequestOptions = {}): Promise<ApiResponse<CommentMutationResponse>>` | POST | `/comments` | [api/comments/create-comment](https://www.tryadeli.com/docs/api/comments/create-comment) |
38
+ | comments | `updateComment(args: { readonly path: Path<'updateComment'>; readonly body: UpdateCommentBody }, options: RequestOptions = {}): Promise<ApiResponse<UpdatedCommentResponse>>` | PATCH | `/comments/{commentId}` | [api/comments/update-comment](https://www.tryadeli.com/docs/api/comments/update-comment) |
39
+ | comments | `deleteComment(args: { readonly path: Path<'deleteComment'>; readonly query: JsonQuery<'deleteComment'> }, options: RequestOptions = {}): Promise<ApiResponse<undefined>>` | DELETE | `/comments/{commentId}` | [api/comments/delete-comment](https://www.tryadeli.com/docs/api/comments/delete-comment) |
40
+ | messages | `listMessages(args: { readonly query?: MessagesQuery } = {}, options: RequestOptions = {}): Promise<ApiResponse<MessagesResponse>>` | GET | `/messages` | [api/messages/list-messages](https://www.tryadeli.com/docs/api/messages/list-messages) |
41
+ | messages | `sendMessage(args: { readonly body: SendMessageBody }, options: RequestOptions = {}): Promise<ApiResponse<SentMessage>>` | POST | `/messages` | [api/messages/send-message](https://www.tryadeli.com/docs/api/messages/send-message) |
42
+ | messages | `sendInstagramMessage(args: { readonly body: SendInstagramMessageBody }, options: RequestOptions = {}): Promise<ApiResponse<SentInstagramMessage>>` | POST | `/messages/instagram` | [api/messages/send-instagram-message](https://www.tryadeli.com/docs/api/messages/send-instagram-message) |
43
+ | analytics | `getInstagramAccountInsights(args: { readonly query: InstagramAccountInsightsQuery }, options: RequestOptions = {}): Promise<ApiResponse<InstagramAccountInsightsResponse>>` | GET | `/analytics/instagram/account-insights` | [api/analytics/instagram-account-insights](https://www.tryadeli.com/docs/api/analytics/instagram-account-insights) |
44
+ | analytics | `getFacebookPageMetrics(args: { readonly query?: FacebookPageMetricsQuery }, options: RequestOptions = {}): Promise<ApiResponse<FacebookPageMetricsResponse>>` | GET | `/analytics/facebook/page-metrics` | [api/analytics/facebook-page-metrics](https://www.tryadeli.com/docs/api/analytics/facebook-page-metrics) |
45
+ | analytics | `getTikTokAccountAnalytics(args: { readonly query: TikTokAccountAnalyticsQuery }, options: RequestOptions = {}): Promise<ApiResponse<TikTokAccountAnalyticsResponse>>` | GET | `/analytics/tiktok/account-analytics` | [api/analytics/tiktok-account-analytics](https://www.tryadeli.com/docs/api/analytics/tiktok-account-analytics) |
46
+ | analytics | `getTikTokAccountInsights(args: { readonly query: TikTokAccountInsightsQuery }, options: RequestOptions = {}): Promise<ApiResponse<TikTokAccountInsightsResponse>>` | GET | `/analytics/tiktok/account-insights` | [api/analytics/tiktok-account-insights](https://www.tryadeli.com/docs/api/analytics/tiktok-account-insights) |
47
+ | analytics | `getTikTokVideoInsights(args: { readonly query: TikTokVideoInsightsQuery }, options: RequestOptions = {}): Promise<ApiResponse<TikTokVideoInsightsResponse>>` | GET | `/analytics/tiktok/video-insights` | [api/analytics/tiktok-post-insights](https://www.tryadeli.com/docs/api/analytics/tiktok-post-insights) |
48
+ | analytics | `getYouTubeChannelInsights(args: { readonly query: YouTubeChannelInsightsQuery }, options: RequestOptions = {}): Promise<ApiResponse<YouTubeChannelInsightsResponse>>` | GET | `/analytics/youtube/channel-insights` | [api/analytics/youtube-channel-insights](https://www.tryadeli.com/docs/api/analytics/youtube-channel-insights) |
49
+ | analytics | `getYouTubeVideoInsights(args: { readonly query: YouTubeVideoInsightsQuery }, options: RequestOptions = {}): Promise<ApiResponse<YouTubeVideoInsightsResponse>>` | GET | `/analytics/youtube/video-insights` | [api/analytics/youtube-video-insights](https://www.tryadeli.com/docs/api/analytics/youtube-video-insights) |
50
+ | ads | `listAdBusinesses(args: { readonly query?: AdBusinessesQuery } = {}, options: RequestOptions = {}): Promise<ApiResponse<ListAdBusinessesResponse>>` | GET | `/ads/businesses` | [api/ads/list-businesses](https://www.tryadeli.com/docs/api/ads/list-businesses) |
51
+ | ads | `listAdAccounts(args: { readonly query?: AdAccountsQuery } = {}, options: RequestOptions = {}): Promise<ApiResponse<ListAdAccountsResponse>>` | GET | `/ads/accounts` | [api/ads/list-ad-accounts](https://www.tryadeli.com/docs/api/ads/list-ad-accounts) |
52
+ | ads | `createAdAccount(args: { readonly query?: CreateAdAccountQuery; readonly body: CreateAdAccountBody }, options: IdempotentOptions): Promise<ApiResponse<CreateAdAccountResponse>>` | POST | `/ads/accounts` | [api/ads/create-ad-account](https://www.tryadeli.com/docs/api/ads/create-ad-account) |
53
+ | ads | `getAdAccount(args: { readonly path: AdAccountPath; readonly query?: AdAccountQuery }, options: RequestOptions = {}): Promise<ApiResponse<AdAccountResponse>>` | GET | `/ads/accounts/{adAccountId}` | [api/ads/get-ad-account](https://www.tryadeli.com/docs/api/ads/get-ad-account) |
54
+ | ads | `updateAdAccount(args: { readonly path: UpdateAdAccountPath; readonly query?: AdAccountQuery; readonly body: UpdateAdAccountBody }, options: RequestOptions = {}): Promise<ApiResponse<AdAccountResponse>>` | PATCH | `/ads/accounts/{adAccountId}` | [api/ads/update-ad-account](https://www.tryadeli.com/docs/api/ads/update-ad-account) |
55
+ | ads | `getAdTree(args: { readonly query: AdTreeQuery }, options: RequestOptions = {}): Promise<ApiResponse<AdTreeResponse>>` | GET | `/ads/tree` | [api/ads/get-campaign-tree](https://www.tryadeli.com/docs/api/ads/get-campaign-tree) |
56
+ | ads | `getAdInsights(args: { readonly query: AdInsightsQuery }, options: RequestOptions = {}): Promise<ApiResponse<AdInsightsResponse>>` | GET | `/ads/insights` | [api/ads/get-insights](https://www.tryadeli.com/docs/api/ads/get-insights) |
57
+ | ads | `listAdLeads(args: { readonly query?: AdLeadsQuery } = {}, options: RequestOptions = {}): Promise<ApiResponse<AdLeadsResponse>>` | GET | `/ads/leads` | [api/ads/list-leads](https://www.tryadeli.com/docs/api/ads/list-leads) |
58
+ | ads | `createCampaign(args: { readonly query?: CreateCampaignQuery; readonly body: CreateCampaignBody }, options: IdempotentOptions): Promise<ApiResponse<CampaignResponse>>` | POST | `/ads/campaigns` | [api/ads/create-campaign](https://www.tryadeli.com/docs/api/ads/create-campaign) |
59
+ | ads | `deleteCampaign(args: { readonly path: DeleteCampaignPath; readonly query: JsonQuery<'deleteCampaign'> }, options: RequestOptions = {}): Promise<ApiResponse<undefined>>` | DELETE | `/ads/campaigns/{campaignId}` | [api/ads/delete-campaign](https://www.tryadeli.com/docs/api/ads/delete-campaign) |
60
+ | ads | `setAdStatus(args: { readonly query?: JsonQuery<'setAdStatus'>; readonly body: SetAdStatusBody }, options: RequestOptions = {}): Promise<ApiResponse<AdStatusResponse>>` | PUT | `/ads/status` | [api/ads/update-status](https://www.tryadeli.com/docs/api/ads/update-status) |
61
+ | ads | `updateAdGroup(args: { readonly path: UpdateAdGroupPath; readonly query?: UpdateAdGroupQuery; readonly body: UpdateAdGroupBody }, options: RequestOptions = {}): Promise<ApiResponse<AdGroupResponse>>` | PATCH | `/ads/ad-groups/{adGroupId}` | [api/ads/update-ad-group](https://www.tryadeli.com/docs/api/ads/update-ad-group) |
62
+ | ads | `listAdTargeting(args: { readonly query: AdTargetingQuery }, options: RequestOptions = {}): Promise<ApiResponse<AdTargetingResponse>>` | GET | `/ads/targeting` | [api/ads/list-targeting](https://www.tryadeli.com/docs/api/ads/list-targeting) |
63
+ | ads | `listAdIdentities(args: { readonly query: AdIdentitiesQuery }, options: RequestOptions = {}): Promise<ApiResponse<AdIdentitiesResponse>>` | GET | `/ads/identities` | [api/ads/list-identities](https://www.tryadeli.com/docs/api/ads/list-identities) |
64
+ | ads | `createAdIdentity(args: { readonly query?: CreateAdIdentityQuery; readonly body: CreateAdIdentityBody }, options: RequestOptions = {}): Promise<ApiResponse<AdIdentityResponse>>` | POST | `/ads/identities` | [api/ads/create-identity](https://www.tryadeli.com/docs/api/ads/create-identity) |
65
+ | usage | `getXUsage(_args: Record<never, never> = {}, options: RequestOptions = {}): Promise<ApiResponse<XUsageResponse>>` | GET | `/usage/x` | [pricing/x](https://www.tryadeli.com/docs/pricing/x) |
66
+ | webhooks | `listWebhooks(args: { readonly query: ListWebhooksQuery }, options: RequestOptions = {}): Promise<ApiResponse<WebhooksResponse>>` | GET | `/webhooks` | [api/webhooks/list-webhooks](https://www.tryadeli.com/docs/api/webhooks/list-webhooks) |
67
+ | webhooks | `createWebhook(args: { readonly body: CreateWebhookBody }, options: RequestOptions = {}): Promise<ApiResponse<CreatedWebhook>>` | POST | `/webhooks` | [api/webhooks/create-webhook](https://www.tryadeli.com/docs/api/webhooks/create-webhook) |
68
+ | webhooks | `deleteWebhook(args: { readonly path: Path<'deleteWebhook'> }, options: RequestOptions = {}): Promise<ApiResponse<undefined>>` | DELETE | `/webhooks/{webhookId}` | [api/webhooks/delete-webhook](https://www.tryadeli.com/docs/api/webhooks/delete-webhook) |
69
+
70
+ ## Compatibility note
71
+
72
+ The SHA-256 above identifies a local, unreleased candidate snapshot. It is provenance, not proof of deployment or npm publication. Regenerate after a reviewed contract update and inspect status, nullability, error, and provider-outcome changes.
@@ -0,0 +1,26 @@
1
+ # Feature availability
2
+
3
+ The SDK types the whole public contract, but not every operation is live on every Adeli deployment. Production (`https://app.tryadeli.com`) offers only approved features. The beta deployment also offers features that are in provider review. Availability is not part of the OpenAPI contract, so the SDK cannot check it at compile time. Read the runtime signals below.
4
+
5
+ ## Production
6
+
7
+ | Platform | Available in production | Beta only |
8
+ | --- | --- | --- |
9
+ | `instagram` | Connect, profile, `listPosts`, messages | Publishing, analytics, comments |
10
+ | `facebook` | Connect, profile, Page selection, `listPosts`, messages | Publishing, analytics, comments |
11
+ | `tiktok` | Connect, profile, `listPosts`, publishing, analytics, comments, refresh | — |
12
+ | `youtube` | Connect, profile, `listPosts`, publishing, analytics, comments, refresh | — |
13
+ | `x` | Connect, profile, publishing, refresh | — |
14
+ | `bluesky` | Connect, profile, publishing | — |
15
+ | `meta_ads` | — | Connect and every `client.ads` operation |
16
+ | `tiktok_ads` | — | Connect and every `client.ads` operation |
17
+
18
+ `whatsapp` appears in the account and connect platform enums, but `startConnection` does not support it yet and answers `422 provider_not_supported`.
19
+
20
+ ## Runtime signals
21
+
22
+ - **Ads.** On a deployment without Meta Ads or TikTok Ads, `client.ads` calls for that network answer `404 not_found`, and `startConnection` with `platform: 'meta_ads'` answers `422 provider_not_configured`.
23
+ - **Instagram and Facebook.** Publishing, analytics, and comments in production depend on Meta-approved permissions. Read the account's `capabilities`, `missingPermissions`, and `missingResources` before calling; a call without them throws `PermissionRequiredError` (`permission_required`).
24
+ - **Other platforms.** A capability the account was not granted reports `false` in `capabilities` and fails with `permission_required`.
25
+
26
+ `baseURL: 'https://beta.tryadeli.com'` reaches the beta deployment. It is an opt-in App Review target, not a release channel, an API version, or the SDK's default origin; a feature there is not yet available in production.
package/docs/client.md ADDED
@@ -0,0 +1,72 @@
1
+ # Client and request semantics
2
+
3
+ `@adeli/sdk` is a server-only SDK for Node.js 24+. It is not supported in browsers, Bun, Deno, or edge runtimes. Construct `Adeli` only in a caller-invoked server function; module import does not read environment variables or make requests.
4
+
5
+ ```ts
6
+ import { Adeli } from '@adeli/sdk';
7
+ const client = new Adeli({ apiKey: process.env.ADELI_API_KEY });
8
+ ```
9
+
10
+ `apiKey` wins over `ADELI_API_KEY`. `baseURL` is an origin (not an origin plus `/api/v1`); production origins must use HTTPS, while HTTP is reserved for explicit local development. `timeoutMs` is a total local deadline and `maxRetries` is opt-in. Timer values must be positive, finite, and no greater than `2_147_483_647` milliseconds (about 24.8 days); larger values are rejected before a request rather than being clamped by Node. The default retry count is zero. With opt-in retries, audited safe reads and profile creation with a stable idempotency key qualify. Daily quota exhaustion, permission/billing/semantic conflicts, and other known non-transient failures are never retried. `listPosts` is conservatively excluded because an X read can be billed; `getXUsage` is a safe read of Adeli's stored usage ledger and does not call X itself, so it is eligible for the ordinary transient-read policy.
11
+
12
+ ## Requests and responses
13
+
14
+ Resource methods use named `{ path, query, body }` arguments. Paths and query values are encoded by the transport. The SDK preserves the wire body: provider-specific casing such as `contentType` and `content_type`, `null`, privacy/visibility, warnings, and partial records remain unchanged. Every successful call returns `{ data, status, headers }`; `202`, `207`, and `204` are valid success statuses (`204` has `data: undefined`). Collection `status: partial` is still a successful response and contains usable records plus per-account errors.
15
+
16
+ Most operations require a profile or connected account scope. Organization-wide account listing is distinct from profile-scoped listing. Permissions/capabilities can be partial or nullable; inspect `missingPermissions`, `missingResources`, `capabilities`, and reconnect guidance rather than assuming a connected account can perform every operation. Public connection starts the typed flow but does not replace Facebook or Meta Ads Page selection or WhatsApp dashboard Embedded Signup. `client.ads` operations take a Meta Ads account (`platform: 'meta_ads'`), which is connected separately from the profile's Facebook account; a Facebook `accountId` returns `account_not_found`.
17
+
18
+ ## Polling and comment iteration
19
+
20
+ Polling helpers are explicit bounded workflows: pass both a positive `intervalMs` and positive total `timeoutMs` (each no greater than `2_147_483_647` milliseconds), and optionally an `AbortSignal`. `waitForConnection`, `waitForInstagramPost`, `waitForTikTokPost`, `waitForFacebookPost`, and `waitForYouTubeVideo` return the terminal API response, including provider failure states; they throw only for local timeout, cancellation, or an unfamiliar provider status. Facebook video `READY`/`ERROR` is separate from a connection's `pending_page_selection` dashboard handoff.
21
+
22
+ ```ts
23
+ import { waitForYouTubeVideo } from '@adeli/sdk/helpers';
24
+
25
+ const status = await waitForYouTubeVideo(
26
+ client,
27
+ { query: { accountId, videoId } },
28
+ { intervalMs: 2_000, timeoutMs: 300_000, signal },
29
+ );
30
+ if (status.data.status === 'failed') throw new Error(status.data.failureReason ?? 'YouTube processing failed');
31
+ ```
32
+
33
+ `iterateCommentPages` and `iterateComments` support TikTok and YouTube only. They accept `maxPages` (default `100`, always finite), optional total `timeoutMs` no greater than `2_147_483_647` milliseconds, and optional `signal`; they stop on no cursor, an empty/repeated cursor, or the page limit.
34
+
35
+ ```ts
36
+ import { iterateCommentPages } from '@adeli/sdk/helpers';
37
+
38
+ for await (const page of iterateCommentPages(
39
+ client,
40
+ { query: { platform: 'youtube', accountId } },
41
+ { maxPages: 100, timeoutMs: 60_000, signal },
42
+ )) {
43
+ for (const comment of page.data.comments) console.log(comment.id);
44
+ // page.data.complete/syncedAt describe cache freshness, not pagination.
45
+ }
46
+ ```
47
+
48
+ The iterator pins the first authoritative `postId` when the initial query omitted one and preserves page cache metadata such as `complete`/`syncedAt`; that metadata describes freshness/completeness of the returned page, not whether `nextCursor` exists.
49
+
50
+ ## Deadlines, cancellation, and errors
51
+
52
+ Pass an `AbortSignal` and/or per-call `timeoutMs` in the second argument. Cancellation stops this client request; it cannot undo work already accepted by Adeli. Polling helpers have a separate total deadline and never fetch a returned OAuth/status URL directly. Treat an aborted write as an ambiguous remote outcome and inspect the provider before resubmitting.
53
+
54
+ Catch `AdeliApiError` for an HTTP/API response. Its `status`, `code`, `details`, and `headers` are preserved; subclasses include `AuthenticationError`, `ValidationError`, `PermissionRequiredError`, and `RateLimitError`. `ConfigurationError`, `NetworkError`, `TimeoutError`, `CancellationError`, and `MalformedResponseError` represent local/client failures. Do not log API keys, OAuth URLs, media, message bodies, or provider payloads by default.
55
+
56
+ Error `details` remain `unknown`: validation arrays and provider-specific objects are not interchangeable. Use the exported `isPermissionRequiredDetails(error.details)` guard before reading known permission fields. It accepts sparse reconnect guidance, validates known fields, and leaves unknown extensions untouched. A false result does not invalidate the API error. Even recognized `reconnect.href` values are advisory data, not URLs the SDK will automatically fetch; restart authorization through the typed connection methods with explicit scope.
57
+
58
+ ## Scope and safety
59
+
60
+ Profile creation accepts an optional idempotency key. Ad-account and campaign creation require keys and do not auto-retry. Publishing has no generic idempotency guarantee. The X usage response is a stored Adeli ledger: `totalCostMicros` and each `operations[].costMicros` are millionths of a US dollar. Preserve these integer values precisely; the SDK does not implicitly convert or round them to dollars. Actual X post/analytics provider calls can incur charges; Ads and destructive calls require separate approval and cleanup planning. The SDK does not claim to enforce every evolving provider-side semantic rule; the API remains authoritative.
61
+
62
+ ## Ads on Meta and TikTok
63
+
64
+ `client.ads` serves both networks. Pass the ads connection's `accountId` (a Meta Ads or a `tiktok_ads` account); with only `profileId`, a profile holding both networks answers `400 ambiguous_account` unless the query also names `platform`. Each response reports its `platform`. TikTok advertiser ids are digits only, and TikTok ad groups arrive under the tree's `adSets`. `createCampaign` takes the Meta body (a Page post) or the TikTok body (video creative, targeting, identity); TikTok objects are created disabled, and `setAdStatus` needs `level` for TikTok. `updateAdGroup`, `listAdTargeting`, `listAdIdentities`, and `createAdIdentity` are TikTok-only; Meta-only operations on a TikTok account, and the reverse, answer `400 unsupported_for_platform`.
65
+
66
+ ## Direct media uploads
67
+
68
+ Pass `Blob`/`File` values in media fields (including Instagram/Facebook media, TikTok video/photos, YouTube video/thumbnail, X media/thread parts, and Bluesky images/video). The posts resource resolves the profile when necessary, calls `createMediaUploads`, PUTs each raw body to its presigned S3 URL with the returned headers unchanged, and sends only staged references to Adeli. Uploads use concurrency three by default; set `concurrency`, `onProgress`, `signal`, or `timeoutMs` in the method options. Presigned PUTs are idempotent and honor the client's bounded retry count; post publication itself is never retried automatically.
69
+
70
+ A file-backed Instagram `createPost` returns a `202` job response. Call `client.posts.getInstagramPostStatus(...)` directly or use bounded `waitForInstagramPost(...)`. Its terminal states are `published`, `failed`, and `outcome_unknown`; the last state is intentionally not retried because Meta may already have accepted the final publish write.
71
+
72
+ Use `client.posts.createMediaUploads(...)` directly when another process manages the PUTs. API keys are sent only to Adeli and never to signed S3 URLs.
@@ -0,0 +1,30 @@
1
+ # SDK maintenance and release handoff
2
+
3
+ This repository is a standalone npm package. The pinned contract in `openapi/` is the only normal build input; builds do not fetch the live API document. `openapi/provenance.json` records whether a snapshot is a deployed public document or an unreleased working-tree candidate. A local candidate must never be described as deployed.
4
+
5
+ ## Reciprocal API/SDK workflow
6
+
7
+ 1. Change the API implementation and response/request contract together in the private core repository, including serialized-handler contract tests and public documentation.
8
+ 2. Export a deterministic candidate OpenAPI document from that working tree. In the SDK checkout, use `npm run sync:openapi -- --file <export> --source-label adeli-core-working-tree`, inspect the diff, then run `npm run generate`, `npm run generate:api-reference`, and `npm run check:generated`. The last command checks both generated types and the API reference.
9
+ 3. Update affected handwritten resources, serializers, helpers, package tests, examples, and the Changeset. `check:contract` must report missing operations or weak success schemas; do not replace it with a count or an always-green stub. It performs local structural checks only (including local references and compositions) and never fetches references. It is not a second semantic compatibility comparator: core's coordinated compatibility review and drift reporting remain required.
10
+ 4. Run generation, contract, typecheck, lint, and tests on Node 24. Validate a real tarball with `npm run build`, `npm run check:package`, `npm run test:package`, and `npm run typecheck:examples`. Consumers must resolve only packed files, never monorepo aliases or source paths. The packed gate runs offline with lifecycle scripts disabled, publint, Are the Types Wrong (`--profile node16` against the existing tarball), ESM/CJS imports, and NodeNext/bundler TypeScript consumers.
11
+ 5. Helpers and uploads must import shared runtime errors through the `@adeli/sdk` self-reference. `tsup` externalizes that self-reference so `splitting: false` cannot duplicate error classes; development typechecking resolves it through the explicit `tsconfig` path aliases. Do not add error-class re-exports to the helper public API.
12
+ 6. When core's per-environment feature availability changes, update [`availability.md`](availability.md) in the same companion change. The OpenAPI contract does not carry availability, so no generated check catches this drift.
13
+ 7. From the core checkout, validate SDK-tagged public MDX against that same tarball with `node scripts/check-sdk-doc-examples.mjs --sdk-package <tarball>`. The full coordinated gate is `yarn sdk:check --sdk-dir <sdk-checkout> --sdk-ref <full-reviewed-commit>` and requires a clean immutable candidate; read-only local preparation uses `yarn sdk:prepare --sdk-dir <sdk-checkout> --working-tree`.
14
+ 8. Return the tested SDK revision, candidate contract hash, package version, tarball filename/hash, exact commands, blocked gates, and unresolved remote-write outcomes to the private change record. Do not copy private PR links, customer data, credentials, or internal implementation details into this repository.
15
+
16
+ The reverse handoff from the SDK to core includes the public method/type changes, compatibility classification, migration guidance, and the candidate provenance. A behavioral API change with no OpenAPI diff still requires review. A required-field, status, error, nullability, or union change requires explicit breaking-change handling.
17
+
18
+ ## Compatibility and rollout
19
+
20
+ The supported runtime is Node.js 24. The package has zero runtime dependencies and exposes ESM and CommonJS entry points. Additive API changes deploy and are verified before a new SDK method is advertised. Breaking changes require a compatibility window or staged API migration; releasing a new SDK major alone does not protect existing API consumers. If publication is delayed, leave the private handoff open and keep existing clients supported.
21
+
22
+ The release workflow is manual and targets the `npm-release` environment; required reviewers, allowed tag refs, and repository protections must be configured and verified externally. Dispatch it on the exact reviewed tag, not its branch: `gh workflow run release.yml --ref v0.1.0 -f reviewed_version=0.1.0 -f release_tag=v0.1.0`. It rejects branch dispatches, mismatched refs, checked-out HEAD/SHA mismatches, package/version/tag mismatches, and artifact metadata/hash mismatches before creating a publish artifact. Validation and OIDC publication retain the same event ref/SHA; the publish job consumes only the tokenless validated tarball. Both jobs run Node 24.22.1, whose bundled npm 11.19.1 meets the trusted-publishing minimum; the workflow checks that exact npm version instead of upgrading npm. Registry configuration, repository protection, npm scope ownership, environment approval, and test-account setup are external prerequisites. The package is MIT-licensed.
23
+
24
+ npm trusted publishing only works for a package that already exists on the registry, so `0.1.0` was published once by hand as a bootstrap: the release workflow ran on the `v0.1.0` tag, its `publish` job was rejected at the environment gate, and the validated CI tarball was downloaded, checked against `artifact.sha256`, and published byte for byte with `npm publish --access public` from an interactive 2FA login. The trusted publisher (`kingscrosslabs/adeli-sdk`, workflow `release.yml`, environment `npm-release`) was then registered and token publishing disabled. That bootstrap is not repeatable: every later release goes through the workflow's OIDC `publish` job only, never a local or token-based `npm publish`.
25
+
26
+ The scheduled/manual drift workflow preserves raw SHA evidence but compares parsed canonical OpenAPI JSON. Formatting/key-order-only changes are reported without failing; meaningful canonical changes and malformed/non-OpenAPI responses fail with review evidence. Fetching is bounded and workflow-only; no drift update, publication, or auto-merge occurs.
27
+
28
+ ## Evidence format
29
+
30
+ Record the exact SDK revision, package version, contract SHA-256, source label, commands and results, tarball filename/hash, known blocked gates, release owner, and follow-up owner. A successful local build is not release readiness, API deployment, npm publication, or runtime smoke evidence. Close the private handoff only after an authorized publication, API availability, public documentation/migration notes, and verified tag/version are confirmed.
@@ -0,0 +1,39 @@
1
+ # Publishing and uploads
2
+
3
+ Publishing is server-side and provider-specific. Pass the real platform payload rather than assuming one universal post model. JSON Instagram image/carousel/Reel/story payloads retain `contentType`, base64 or supported URL media, captions, collaborators, user tags, and Reel feed options. Facebook text/image/video/Reel, TikTok JSON or multipart, YouTube multipart, X, and Bluesky have different fields and outcomes; preserve their response fields unchanged.
4
+
5
+ ```ts
6
+ import { Adeli } from '@adeli/sdk';
7
+ import { fileFromPath } from '@adeli/sdk/uploads';
8
+
9
+ export async function uploadYouTube(input: { accountId: string; path: string }) {
10
+ const client = new Adeli({ apiKey: process.env.ADELI_API_KEY });
11
+ const response = await client.posts.uploadYouTubeVideo({ body: {
12
+ accountId: input.accountId,
13
+ video: await fileFromPath(input.path, { contentType: 'video/mp4' }),
14
+ title: 'Example', privacy_status: 'private', made_for_kids: false,
15
+ } });
16
+ return response; // 202 is accepted work, not proof of public visibility.
17
+ }
18
+ ```
19
+
20
+ Use `Blob`/`File` values or the Node file-backed adapter. File-backed uploads avoid silently base64-encoding multi-GB media; supported URL media is sent to Adeli, not fetched by this SDK. Multipart field names, repeated `photos[]`, booleans, zero values, thumbnail, privacy, kids, scheduling, and retained filenames are serialized as documented by each operation. The current candidate API caps are: TikTok multipart accepts one video up to 1,000,000,000 bytes, or 1–35 photos with each photo at most 20 MiB; YouTube multipart accepts a video up to 4 GiB and an optional JPEG/PNG thumbnail up to 50 MiB, with the total multipart request capped at 4 GiB including fields and multipart overhead. JSON `POST /api/v1/posts` is capped at 96 MiB, and an inline base64 video must decode to at most 64 MiB. These are local candidate API caps, not a global provider limit or SDK-side preflight; provider restrictions and validation remain authoritative, and file plus thumbnail plus fields must fit the YouTube total request cap. See the public [TikTok publishing page](https://www.tryadeli.com/docs/api/posts/publish-tiktok), [YouTube file upload page](https://www.tryadeli.com/docs/api/posts/upload-youtube-video-file), and [YouTube upload guide](https://developers.google.com/youtube/v3/guides/implementation/videos) for the current candidate and provider behavior. The SDK does not expose dashboard-only Instagram/Facebook multipart routes.
21
+
22
+ A `201`/`200` result may be synchronous; `202` means accepted processing; it does not guarantee final provider visibility. File-backed Instagram requests use staged references and return a durable job: use `waitForInstagramPost` or `client.posts.getInstagramPostStatus`, and treat `outcome_unknown` as terminal without automatic resubmission. Use `@adeli/sdk/helpers` status helpers explicitly. TikTok `SEND_TO_USER_INBOX` is delivery to the creator inbox, not public publication. `PUBLISH_COMPLETE` is terminal even without public IDs. YouTube `published` means provider processing completed, not that a video is public: retain `privacyStatus`, `publishAt`, and permalink. Facebook video status `PROCESSING` is provider video processing and terminates as `READY` or `ERROR`; it is not the OAuth connection status `pending_page_selection`. A Facebook connection can separately require Page selection through the dashboard before publishing, and the SDK does not complete that handoff.
23
+
24
+ Thread and aggregate operations may return `207` with successful records and per-item/provider failures. Consume successful results and surface errors; never automatically retry a partial or ambiguous write. A local timeout or cancellation only ends the SDK request and leaves remote outcome unknown. Inspect the provider/API before any resubmission. X reads can cost money. Both `createAdAccount` and `createCampaign` require an idempotency key and neither auto-retries; profile creation's key is optional.
25
+
26
+ ## Instagram DMs and private replies
27
+
28
+ `client.messages.sendInstagramMessage` sends an Instagram DM to a person (`recipient: { id }`, their Instagram-scoped id) or a private reply to a comment (`recipient: { commentId }`), with up to three optional `postback` or `url` buttons. `sendMessage` is unchanged and does not take buttons or comment recipients. The `201` result's `recipient` is the person's Instagram-scoped id, including for a private reply, so a later DM can use `recipient: { id }`. A comment's `authorId` from `listComments` is the same id.
29
+
30
+ ```ts
31
+ const reply = await client.messages.sendInstagramMessage({ body: {
32
+ accountId,
33
+ recipient: { commentId },
34
+ text: 'Thanks for commenting! Want the link?',
35
+ buttons: [{ type: 'postback', title: 'Send it', payload: 'send_link' }],
36
+ } });
37
+ ```
38
+
39
+ Instagram allows one private reply per comment, within 7 days of the comment, and only for accounts connected with Instagram Login. A second reply to the same comment fails with `409 private_reply_already_sent`. After a private reply, send a follow-up DM only once the person responds, for example by tapping a postback button; otherwise the send fails with `409 window_closed`. An account connected with Facebook Login returns `400 unsupported_for_platform` for a private reply. Like every write, the SDK never retries a send automatically.