apick-api 3.5.0 → 3.6.2

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.
package/CHANGELOG.md CHANGED
@@ -1,4 +1,29 @@
1
- # Changelog
1
+ # Changelog
2
+
3
+ ## 3.6.2 — 2026-10-02
4
+
5
+ - `TTS_VOICE_IDS`를 현재 지원하는 14개로 갱신했습니다. 제공이 종료된 `v2_ann_f_30s_01`, `v2_f_teen_01`을 상수·타입·문서에서 제거했으며, 이 값을 `voiceId`로 넘기면 요청 전에 `RangeError`가 발생합니다.
6
+ - Update `TTS_VOICE_IDS` to the 14 currently supported voices. The retired `v2_ann_f_30s_01` and `v2_f_teen_01` are removed from the constant, types and docs; passing them as `voiceId` now throws a `RangeError` before the request is sent.
7
+ - TTS 요금이 100자당 10포인트에서 "100자까지 30포인트, 이후 100자당 10포인트"로 바뀌었습니다(100자 30P, 101자 40P, 800자 100P). 한국어 읽기 자동 변환이 이 요금에 포함되며, 과금 기준은 계속 보낸 원문의 글자 수입니다.
8
+ - TTS pricing changed from 10 points per 100 characters to 30 points for up to 100 characters plus 10 points for each additional 100 characters (100 chars 30P, 101 chars 40P, 800 chars 100P). Automatic Korean reading conversion is included, and billing is still based on the character count of the text you send.
9
+ - 모든 TTS 요청에 한국어 읽기 전처리가 자동 적용되어 추가 옵션이나 별도 Skill 호출이 필요하지 않음을 README와 한국어·영어 가이드에 명시했습니다. 원문 기준 요금과 ASS 자막 표기는 유지되며, 정규화에 실패하면 원문으로 합성합니다. 공개 메서드와 요청·응답 형식은 변경하지 않았습니다.
10
+ - Clarify in the README and Korean/English guides that every TTS request automatically applies Korean reading normalization, with no extra option or separate Skill call. Billing and ASS subtitle spelling remain based on the original text; failed normalization falls back to synthesis from that text. No public method or request/response format changes.
11
+
12
+ ## 3.6.1 — 2026-10-02
13
+
14
+ - `createTtsJob()`으로 접수한 문장의 숫자·단위·기호·영문 약어를 문맥에 맞는 한글 읽기로 자동 변환해 합성한다는 안내를 README와 한국어·영어 가이드에 추가했습니다. 과금 글자 수와 요금은 보낸 원문 기준이고 요청·응답 형식은 그대로이며, ASS 자막은 보낸 원문 표기로 제공됩니다. 문서만 바뀌었습니다.
15
+ - Document in the README and Korean/English guides that text passed to `createTtsJob()` has its numbers, units, symbols and English abbreviations automatically converted into context-appropriate Korean readings before synthesis. Billing is based on the text you send, request and response formats are unchanged, and ASS subtitles keep the text as you sent it. Documentation only.
16
+
17
+ ## 3.6.0 — 2026-10-02
18
+
19
+ - Skills 메서드 8종을 추가했습니다: `searchSkills()`, `getSkill()`, `quoteSkill()`, `runSkill()`, `getSkillRun()`, `getSkillRunResult()`, `cancelSkillRun()`, `skillUsage()`. 요청·응답이 JSON 이며 응답은 봉투 없이 `data` 에 담깁니다.
20
+ - Add eight Skills methods to search, inspect, quote, run, read, fetch the result of, cancel and list Skill runs. Requests and responses are JSON; responses are returned as-is in `data`.
21
+ - `runSkill()` 은 `idempotencyKey` 가 필수이며 `Idempotency-Key` 헤더로 보냅니다. 결과가 약속한 형식으로 반환된 실행만 차감됩니다.
22
+ - 생성형 AI 를 쓰는 Skill 은 실행마다 실제 사용량만큼 금액이 달라집니다. `quoteSkill()` 이 예상 금액(`estimated_points`)과 최대 금액(`max_points`)을 돌려주며, `maxCostPoints` 에는 `max_points` 를 넘기세요.
23
+ - For Skills that use generative AI the charge varies per run with actual usage. `quoteSkill()` returns `estimated_points` and `max_points`; pass `max_points` as `maxCostPoints`.
24
+ - `runSkill()` requires `idempotencyKey`, sent as the `Idempotency-Key` header. Only runs that return a result in the promised format are charged.
25
+ - `ApickApiError` 에 선택적 `details` 를 추가했습니다. Skills 오류 코드는 `serviceCode` 로 전달됩니다. `SKILL_CATEGORIES` 상수와 Skills 타입(`SkillSummary`, `SkillDetail`, `SkillQuote`, `SkillRun` 등)을 공개했습니다.
26
+ - Add optional `details` to `ApickApiError`; Skills error codes are surfaced through `serviceCode`. Export `SKILL_CATEGORIES` and the Skills types.
2
27
 
3
28
  ## 3.5.0 — 2026-09-30
4
29
 
package/README.md CHANGED
@@ -101,6 +101,40 @@ Leave the allowed-IP list blank for unrestricted access. To restrict access, reg
101
101
  | `requestHealthCheckup(input)` / `getHealthCheckup(transactionId)` | 국가 건강검진 결과 조회 / National health checkup results | JSON |
102
102
  | `requestCashReceiptDeduction(input)` / `getCashReceiptDeduction(transactionId)` | 현금영수증 소득공제 내역 / Cash receipt income deductions | JSON |
103
103
  | `requestTaxReturnHistory(input)` / `getTaxReturnHistory(transactionId)` | 국세 신고내역 조회 / National tax return history | JSON |
104
+ | `searchSkills(options)` / `getSkill(skillId)` | Skill 검색·상세 / Search and inspect Skills | JSON |
105
+ | `quoteSkill(skillId, input, options)` | Skill 견적(무료) / Quote a run (free) | JSON |
106
+ | `runSkill(skillId, input, options)` | Skill 실행 / Run a Skill | JSON |
107
+ | `getSkillRun(runId)` / `getSkillRunResult(runId)` / `cancelSkillRun(runId)` | 실행 조회·결과·취소 / Read, fetch result, cancel | JSON |
108
+ | `skillUsage(options)` | 내 Skill 실행 내역 / My Skill runs | JSON |
109
+
110
+ ## Skills
111
+
112
+ 검수를 거친 Skill 을 검색하고 실행합니다. 결과가 약속한 형식으로 반환된 실행만 포인트가 차감되고, 실패·시간초과·취소는 차감되지 않습니다. 생성형 AI 를 쓰는 Skill 은 실행마다 실제 사용량만큼 금액이 달라지므로, 실행 전에 `quoteSkill()` 로 예상 금액(`estimated_points`)과 최대 금액(`max_points`)을 확인하세요. 1회 이상 결제한 계정에서 실행할 수 있습니다.
113
+ Search and run reviewed Skills. Points are charged only when a result in the promised format is returned; failures, timeouts and cancellations are not charged. For Skills that use generative AI the amount varies per run with actual usage, so call `quoteSkill()` first to get the estimated (`estimated_points`) and maximum (`max_points`) amount. Running requires an account with at least one payment.
114
+
115
+ ```js
116
+ import { randomUUID } from 'node:crypto';
117
+
118
+ const found = await apick.searchSkills({ query: '상품명', limit: 5 });
119
+ const skillId = found.data.items[0].skill_id;
120
+ const detail = await apick.getSkill(skillId); // input_schema, output_schema, price_points, estimated_points
121
+ const input = { product_name: '튼튼한 접이식 우산' };
122
+ const quote = (await apick.quoteSkill(skillId, input)).data; // 무료 / free: estimated_points, max_points
123
+
124
+ const idempotencyKey = randomUUID(); // 재시도할 때 같은 값을 다시 씁니다 / reuse on retry
125
+ let run = (await apick.runSkill(skillId, input, { idempotencyKey, quoteId: quote.quote_id, maxCostPoints: quote.max_points })).data;
126
+ while (!['succeeded', 'failed', 'timed_out', 'cancelled'].includes(run.status)) {
127
+ await new Promise(resolve => setTimeout(resolve, 2000));
128
+ run = (await apick.getSkillRun(run.run_id)).data;
129
+ }
130
+ console.log(run.status, run.billing, run.result);
131
+ ```
132
+
133
+ Skills 응답은 다른 API 와 달리 봉투 없이 그대로 `data` 에 담깁니다. 과금 상태는 `data.billing`(`reserved`·`captured`·`released`…)에서 확인하며 `meta.cost` 는 채워지지 않습니다. 응답을 받지 못했을 때는 **같은 `idempotencyKey`** 로 다시 호출하세요. 포인트는 한 번만 차감됩니다. 같은 키에 다른 입력을 보내면 `IDEMPOTENCY_CONFLICT` 로 거부됩니다.
134
+ Skills responses are returned as-is in `data`. Read the charge from `data.billing`; `meta.cost` is not populated. If a response is lost, call again with the **same `idempotencyKey`** — points are charged once. The same key with a different input is rejected with `IDEMPOTENCY_CONFLICT`.
135
+
136
+ 접수 전에 거절된 요청은 `ApickApiError` 로 던져지며 `serviceCode` 에 `INVALID_INPUT`·`INSUFFICIENT_POINTS`·`PRICE_EXCEEDS_LIMIT`·`RATE_LIMITED` 같은 코드가, `details` 에 부가 정보가 담깁니다. 접수된 뒤 실패한 실행은 예외가 아니라 `status` 와 `failure_code` 로 확인합니다.
137
+ Requests rejected before acceptance throw `ApickApiError` with the code in `serviceCode` and extra information in `details`. A run that fails after acceptance is reported through `status` and `failure_code`, not an exception.
104
138
 
105
139
  ## JSON 결과 / JSON results
106
140
 
@@ -197,9 +231,9 @@ await excel.save('./scores.xlsx');
197
231
 
198
232
  The legacy synchronous TTS API has retired. Create a Korean narration job, poll every 2–5 seconds until it is `completed`, then download the MP3 result once.
199
233
 
200
- TTS supports 16 voice IDs. Use `TTS_VOICE_IDS` and the developer guide for the current list.
234
+ TTS supports 14 voice IDs. Use `TTS_VOICE_IDS` and the developer guide for the current list.
201
235
 
202
- `v2_ann_m_30s_01`, `v2_ann_m_30s_02`, `v2_ann_m_30s_04`, `v2_ann_m_30s_05`, `v2_ann_f_30s_01`, `v2_ann_f_30s_02`, `v2_ann_f_30s_03`, `v2_ann_f_30s_04`, `v2_ann_f_30s_05`, `v2_m_teen_01`, `v2_m_young_01`, `v2_m_mid_01`, `v2_m_senior_01`, `v2_f_teen_01`, `v2_f_young_01`, `v2_f_senior_01`
236
+ `v2_ann_m_30s_01`, `v2_ann_m_30s_02`, `v2_ann_m_30s_04`, `v2_ann_m_30s_05`, `v2_ann_f_30s_02`, `v2_ann_f_30s_03`, `v2_ann_f_30s_04`, `v2_ann_f_30s_05`, `v2_m_teen_01`, `v2_m_young_01`, `v2_m_mid_01`, `v2_m_senior_01`, `v2_f_young_01`, `v2_f_senior_01`
203
237
 
204
238
  ```js
205
239
  const created = await apick.createTtsJob('오늘의 이야기를 시작합니다.', {
@@ -225,6 +259,10 @@ if (job.data.status === 'completed') {
225
259
 
226
260
  The charge is final when the job is accepted. Cancellation is allowed while `waiting` or `processing`. The MP3 and ASS subtitles can each be downloaded once. Starting either download immediately consumes that server copy, so an interrupted transfer cannot be downloaded again.
227
261
 
262
+ `createTtsJob()`으로 접수한 문장의 숫자·단위·기호·영문은 문맥에 맞는 한글 읽기로 자동 변환한 뒤 음성을 생성합니다(예: `5번 버스` → 오 번 버스, `버튼을 5번` → 다섯 번, `-5℃` → 영하 오 도, `인증번호 105028` → 한 자리씩). 모든 TTS 요청에 자동 적용되므로 추가 옵션이나 별도 Skill 호출이 필요하지 않습니다. 과금 글자 수와 요금은 보낸 원문 기준이며 요청·응답 형식은 그대로입니다. 요금은 100자까지 30포인트이고 이후 100자마다 10포인트가 추가되며, 자동 변환에 따른 추가 요금은 없습니다. 읽는 법을 직접 정하려면 한글로 풀어 써서 보내세요. ASS 자막은 보낸 원문 표기로 제공됩니다. 정규화에 실패하면 원문으로 음성을 생성합니다.
263
+
264
+ Numbers, units, symbols and English text in the text passed to `createTtsJob()` are automatically converted into context-appropriate Korean readings before synthesis (for example, `5번 버스` is read as "오 번 버스", `버튼을 5번` as "다섯 번", `-5℃` as "영하 오 도", and `인증번호 105028` digit by digit). This applies automatically to every TTS request; no extra option or separate Skill call is needed. The billed character count and price are based on the text you send, and the request and response formats are unchanged. The price is 30 points for up to 100 characters plus 10 points for each additional 100 characters, with no extra charge for the automatic conversion. To choose a reading yourself, spell it out in Hangul. ASS subtitles keep the text as you sent it. If normalization fails, speech is generated from the original text.
265
+
228
266
  ## 신분증 마스킹 / Identity masking
229
267
 
230
268
  ```js
package/docs/guide.en.md CHANGED
@@ -86,9 +86,11 @@ await client.ocr(bytes, {
86
86
 
87
87
  ## Generated files
88
88
 
89
- TTS supports 16 voice IDs. Use `TTS_VOICE_IDS` and the developer guide for the current list.
89
+ TTS supports 14 voice IDs. Use `TTS_VOICE_IDS` and the developer guide for the current list.
90
90
 
91
- `v2_ann_m_30s_01`, `v2_ann_m_30s_02`, `v2_ann_m_30s_04`, `v2_ann_m_30s_05`, `v2_ann_f_30s_01`, `v2_ann_f_30s_02`, `v2_ann_f_30s_03`, `v2_ann_f_30s_04`, `v2_ann_f_30s_05`, `v2_m_teen_01`, `v2_m_young_01`, `v2_m_mid_01`, `v2_m_senior_01`, `v2_f_teen_01`, `v2_f_young_01`, `v2_f_senior_01`
91
+ `v2_ann_m_30s_01`, `v2_ann_m_30s_02`, `v2_ann_m_30s_04`, `v2_ann_m_30s_05`, `v2_ann_f_30s_02`, `v2_ann_f_30s_03`, `v2_ann_f_30s_04`, `v2_ann_f_30s_05`, `v2_m_teen_01`, `v2_m_young_01`, `v2_m_mid_01`, `v2_m_senior_01`, `v2_f_young_01`, `v2_f_senior_01`
92
+
93
+ Numbers, units, symbols and English text in the text passed to `createTtsJob()` are automatically converted into context-appropriate Korean readings before synthesis (for example, `5번 버스` is read as "오 번 버스", `버튼을 5번` as "다섯 번", `-5℃` as "영하 오 도", and `인증번호 105028` digit by digit). This applies automatically to every TTS request; no extra option or separate Skill call is needed. The billed character count and price are based on the text you send, and the request and response formats are unchanged. The price is 30 points for up to 100 characters plus 10 points for each additional 100 characters, with no extra charge for the automatic conversion. To choose a reading yourself, spell it out in Hangul. ASS subtitles keep the text as you sent it. If normalization fails, speech is generated from the original text.
92
94
 
93
95
  ```js
94
96
  const screenshot = await client.screenshot('https://example.com');
@@ -270,6 +272,48 @@ Sources: [APICK development guide](https://apick.app/dev_guide/data_health_check
270
272
 
271
273
  `authProvider` is one of the 13 values in `AUTH_PROVIDERS` (kakao, naver, toss, pass, samsung, kb, shinhan, hana, woori, ibk, nh, kakaobank, banksalad). Acceptance is billed at a flat rate; the result is billed only on its first return and free to re-poll afterward. `requestEmployment` takes an optional `insuranceYears` (1-3), `requestPersonalIncome` takes `incomeYears` (1-5), and `requestNpsJoinHistory` takes optional `from`/`to` (`YYYY-MM`). `requestCashReceiptDeduction` takes optional `incomeYears` (1-3) and `requestTaxReturnHistory` takes optional `years` (1-10). The remaining products are `requestDrivingLicense` and `requestHealthCheckup`.
272
274
 
275
+ ## Skills
276
+
277
+ Search and run reviewed Skills. Each Skill fixes its input format (`input_schema`), output format (`output_schema`) and base amount (`price_points`). A run is charged only when a result in the promised format is returned.
278
+
279
+ For Skills that use generative AI (`usage_priced: true`) the actual usage of each run is added to the base amount, so the charge varies per run. `quoteSkill()` returns the estimated amount (`estimated_points`) and the maximum amount (`max_points`). The maximum is reserved when the run is accepted; when it finishes only the actual amount is charged and the rest is returned. Read the actual charge from `run.billing.charged_points`.
280
+
281
+ ```js
282
+ import { randomUUID } from 'node:crypto';
283
+
284
+ const found = await client.searchSkills({ query: 'product name', limit: 5 });
285
+ const skillId = found.data.items[0].skill_id;
286
+ const detail = await client.getSkill(skillId);
287
+ const input = { product_name: 'Sturdy folding umbrella' };
288
+
289
+ const quote = await client.quoteSkill(skillId, input); // free, valid for 5 minutes
290
+ const idempotencyKey = randomUUID(); // reuse the same value when retrying
291
+ let run = (await client.runSkill(skillId, input, {
292
+ idempotencyKey, quoteId: quote.data.quote_id, maxCostPoints: quote.data.max_points
293
+ })).data;
294
+ while (!['succeeded', 'failed', 'timed_out', 'cancelled'].includes(run.status)) {
295
+ await new Promise(resolve => setTimeout(resolve, 2000));
296
+ run = (await client.getSkillRun(run.run_id)).data;
297
+ }
298
+ console.log(run.status, run.billing, run.result);
299
+ ```
300
+
301
+ | Method | Description |
302
+ | --- | --- |
303
+ | `searchSkills({ query, category, cursor, limit })` | Search. `limit` 1-20; `category` is one of `SKILL_CATEGORIES` |
304
+ | `getSkill(skillId)` | Input and output formats, price, limits, examples |
305
+ | `quoteSkill(skillId, input, { version })` | Validates the input and reports the points to be charged. Does not run |
306
+ | `runSkill(skillId, input, { idempotencyKey, quoteId, maxCostPoints, version, waitSeconds })` | Run. `idempotencyKey` is required; `waitSeconds` 0-20 (default 20) |
307
+ | `getSkillRun(runId)` / `getSkillRunResult(runId)` | Run status (with the result once succeeded) / result only. Results are kept for 7 days |
308
+ | `cancelSkillRun(runId)` | Cancel an unfinished run. Cancelled runs are not charged |
309
+ | `skillUsage({ cursor, limit })` | Your runs. `limit` 1-50 |
310
+
311
+ - Skills responses are returned as-is in `data`. Read the charge from `data.billing.status` (`reserved`, `captured`, `released`, `partially_refunded`, `refunded`); `meta.cost` is not populated.
312
+ - If a run does not finish within 20 seconds it comes back as `queued` or `running`; poll `getSkillRun`.
313
+ - If a response is lost, call again with the same `idempotencyKey`. Points are charged once. The same key with a different input is rejected with `IDEMPOTENCY_CONFLICT`.
314
+ - Rejections before acceptance throw `ApickApiError` (`serviceCode` such as `INVALID_INPUT`, `INSUFFICIENT_POINTS`, `PAYMENT_REQUIRED`, `PRICE_EXCEEDS_LIMIT`, `RATE_LIMITED`; extra information in `details`). Failures after acceptance are reported through `status` and `failure_code` (`EXECUTION_FAILED`, `OUTPUT_INVALID`, `TIMED_OUT`, `CANCELLED`, `UPSTREAM_UNAVAILABLE`).
315
+ - Skills with `uses_generative_ai: true` produce results with generative AI. Verify them before relying on them.
316
+
273
317
  ## Errors and retries
274
318
 
275
319
  `ApickApiError` includes public error information: `code`, optional `serviceCode`, `status`, and `message`. The SDK does not retry automatically because a retry could duplicate an API call and its charge. If your application needs retries, decide explicitly after checking the error code and whether the operation is safe to repeat.
package/docs/guide.ko.md CHANGED
@@ -86,9 +86,11 @@ await client.ocr(bytes, {
86
86
 
87
87
  ## 파일 생성
88
88
 
89
- TTS는 16개 목소리 ID를 지원합니다. 정확한 목록은 `TTS_VOICE_IDS` 상수와 개발가이드에서 확인합니다.
89
+ TTS는 14개 목소리 ID를 지원합니다. 정확한 목록은 `TTS_VOICE_IDS` 상수와 개발가이드에서 확인합니다.
90
90
 
91
- `v2_ann_m_30s_01`, `v2_ann_m_30s_02`, `v2_ann_m_30s_04`, `v2_ann_m_30s_05`, `v2_ann_f_30s_01`, `v2_ann_f_30s_02`, `v2_ann_f_30s_03`, `v2_ann_f_30s_04`, `v2_ann_f_30s_05`, `v2_m_teen_01`, `v2_m_young_01`, `v2_m_mid_01`, `v2_m_senior_01`, `v2_f_teen_01`, `v2_f_young_01`, `v2_f_senior_01`
91
+ `v2_ann_m_30s_01`, `v2_ann_m_30s_02`, `v2_ann_m_30s_04`, `v2_ann_m_30s_05`, `v2_ann_f_30s_02`, `v2_ann_f_30s_03`, `v2_ann_f_30s_04`, `v2_ann_f_30s_05`, `v2_m_teen_01`, `v2_m_young_01`, `v2_m_mid_01`, `v2_m_senior_01`, `v2_f_young_01`, `v2_f_senior_01`
92
+
93
+ `createTtsJob()`으로 접수한 문장의 숫자·단위·기호·영문은 문맥에 맞는 한글 읽기로 자동 변환한 뒤 음성을 생성합니다(예: `5번 버스` → 오 번 버스, `버튼을 5번` → 다섯 번, `-5℃` → 영하 오 도, `인증번호 105028` → 한 자리씩). 모든 TTS 요청에 자동 적용되므로 추가 옵션이나 별도 Skill 호출이 필요하지 않습니다. 과금 글자 수와 요금은 보낸 원문 기준이며 요청·응답 형식은 그대로입니다. 요금은 100자까지 30포인트이고 이후 100자마다 10포인트가 추가되며, 자동 변환에 따른 추가 요금은 없습니다. 읽는 법을 직접 정하려면 한글로 풀어 써서 보내세요. ASS 자막은 보낸 원문 표기로 제공됩니다. 정규화에 실패하면 원문으로 음성을 생성합니다.
92
94
 
93
95
  ```js
94
96
  const screenshot = await client.screenshot('https://example.com');
@@ -272,6 +274,48 @@ try {
272
274
 
273
275
  `authProvider`는 `AUTH_PROVIDERS`(13종: kakao, naver, toss, pass, samsung, kb, shinhan, hana, woori, ibk, nh, kakaobank, banksalad) 중 하나입니다. 접수는 정액 과금, 결과는 최초 반환에서만 과금되며 재조회는 무료입니다. `requestEmployment`는 `insuranceYears`(1~3), `requestPersonalIncome`은 `incomeYears`(1~5), `requestNpsJoinHistory`는 `from`/`to`(`YYYY-MM`) 선택 입력을 받습니다. `requestCashReceiptDeduction`은 `incomeYears`(1~3), `requestTaxReturnHistory`는 `years`(1~10) 선택 입력을 받습니다. 나머지 상품은 `requestDrivingLicense`, `requestHealthCheckup`입니다.
274
276
 
277
+ ## Skills
278
+
279
+ 검수를 거친 Skill 을 검색하고 실행합니다. Skill 마다 입력 형식(`input_schema`)·결과 형식(`output_schema`)·기본 금액(`price_points`)이 정해져 있으며, 결과가 약속한 형식으로 반환된 실행만 차감됩니다.
280
+
281
+ 생성형 AI 를 쓰는 Skill(`usage_priced: true`)은 기본 금액에 그 실행의 실제 사용량이 더해져 실행마다 금액이 달라집니다. `quoteSkill()` 은 예상 금액(`estimated_points`)과 최대 금액(`max_points`)을 돌려줍니다. 실행을 접수할 때 최대 금액을 예약하고, 끝나면 실제 금액만 차감한 뒤 나머지를 돌려줍니다. 실제 차감액은 `run.billing.charged_points` 에서 확인합니다.
282
+
283
+ ```js
284
+ import { randomUUID } from 'node:crypto';
285
+
286
+ const found = await client.searchSkills({ query: '상품명', limit: 5 });
287
+ const skillId = found.data.items[0].skill_id;
288
+ const detail = await client.getSkill(skillId);
289
+ const input = { product_name: '튼튼한 접이식 우산' };
290
+
291
+ const quote = await client.quoteSkill(skillId, input); // 무료. 5분 동안 유효
292
+ const idempotencyKey = randomUUID(); // 재시도할 때 같은 값을 다시 씁니다
293
+ let run = (await client.runSkill(skillId, input, {
294
+ idempotencyKey, quoteId: quote.data.quote_id, maxCostPoints: quote.data.max_points
295
+ })).data;
296
+ while (!['succeeded', 'failed', 'timed_out', 'cancelled'].includes(run.status)) {
297
+ await new Promise(resolve => setTimeout(resolve, 2000));
298
+ run = (await client.getSkillRun(run.run_id)).data;
299
+ }
300
+ console.log(run.status, run.billing, run.result);
301
+ ```
302
+
303
+ | 메서드 | 설명 |
304
+ | --- | --- |
305
+ | `searchSkills({ query, category, cursor, limit })` | 검색. `limit` 1~20, `category` 는 `SKILL_CATEGORIES` 중 하나 |
306
+ | `getSkill(skillId)` | 입력·결과 형식, 가격, 처리 상한, 예제 |
307
+ | `quoteSkill(skillId, input, { version })` | 입력 검사와 차감될 포인트 확인. 실행하지 않습니다 |
308
+ | `runSkill(skillId, input, { idempotencyKey, quoteId, maxCostPoints, version, waitSeconds })` | 실행. `idempotencyKey` 필수, `waitSeconds` 0~20(기본 20) |
309
+ | `getSkillRun(runId)` / `getSkillRunResult(runId)` | 실행 상태(성공하면 결과 포함) / 결과만 조회. 결과는 7일 동안 보관 |
310
+ | `cancelSkillRun(runId)` | 끝나지 않은 실행 취소. 취소된 실행은 차감되지 않습니다 |
311
+ | `skillUsage({ cursor, limit })` | 내 실행 내역. `limit` 1~50 |
312
+
313
+ - Skills 응답은 봉투 없이 `data` 에 그대로 담기며, 과금 상태는 `data.billing.status`(`reserved`·`captured`·`released`·`partially_refunded`·`refunded`)로 확인합니다. `meta.cost` 는 채워지지 않습니다.
314
+ - 20초 안에 끝나지 않으면 `status` 가 `queued`·`running` 인 채로 돌아오므로 `getSkillRun` 으로 확인하세요.
315
+ - 응답을 받지 못했을 때는 같은 `idempotencyKey` 로 다시 호출하세요. 포인트는 한 번만 차감됩니다. 같은 키에 다른 입력을 보내면 `IDEMPOTENCY_CONFLICT` 입니다.
316
+ - 접수 전 거절은 `ApickApiError`(`serviceCode`: `INVALID_INPUT`·`INSUFFICIENT_POINTS`·`PAYMENT_REQUIRED`·`PRICE_EXCEEDS_LIMIT`·`RATE_LIMITED` 등, `details` 에 부가 정보)로, 접수 뒤 실패는 `status` 와 `failure_code`(`EXECUTION_FAILED`·`OUTPUT_INVALID`·`TIMED_OUT`·`CANCELLED`·`UPSTREAM_UNAVAILABLE`)로 확인합니다.
317
+ - `uses_generative_ai` 가 true 인 Skill 은 생성형 AI 로 결과를 만듭니다. 중요한 판단에 쓰기 전에 확인하세요.
318
+
275
319
  ## 오류와 재시도
276
320
 
277
321
  `ApickApiError`에는 공개 오류 정보인 `code`, `serviceCode`, `status`, `message`가 포함됩니다. SDK는 중복 호출과 중복 과금을 방지하기 위해 자동 재시도를 하지 않습니다. 재시도가 필요하면 작업의 멱등성과 오류 코드를 확인한 뒤 애플리케이션에서 명시적으로 결정하세요.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apick-api",
3
- "version": "3.5.0",
3
+ "version": "3.6.2",
4
4
  "description": "Official zero-dependency Node.js client for APICK data, AI, and image APIs. 에이픽 데이터·AI·이미지 API 공식 Node.js SDK.",
5
5
  "type": "module",
6
6
  "main": "./src/index.cjs",
package/src/index.cjs CHANGED
@@ -7,7 +7,7 @@ const MAX_IMAGE_AI_BYTES = 50 * 1024 * 1024;
7
7
  const IMAGE_AI_SIZES = Object.freeze(['1024x1024', '1536x1024', '1024x1536', '1152x864', '864x1152']);
8
8
  const IMAGE_AI_SIZE_SET = new Set(IMAGE_AI_SIZES);
9
9
  const TTS_VOICE_IDS = Object.freeze([
10
- 'v2_ann_m_30s_01', 'v2_ann_m_30s_02', 'v2_ann_m_30s_04', 'v2_ann_m_30s_05', 'v2_ann_f_30s_01', 'v2_ann_f_30s_02', 'v2_ann_f_30s_03', 'v2_ann_f_30s_04', 'v2_ann_f_30s_05', 'v2_m_teen_01', 'v2_m_young_01', 'v2_m_mid_01', 'v2_m_senior_01', 'v2_f_teen_01', 'v2_f_young_01', 'v2_f_senior_01'
10
+ 'v2_ann_m_30s_01', 'v2_ann_m_30s_02', 'v2_ann_m_30s_04', 'v2_ann_m_30s_05', 'v2_ann_f_30s_02', 'v2_ann_f_30s_03', 'v2_ann_f_30s_04', 'v2_ann_f_30s_05', 'v2_m_teen_01', 'v2_m_young_01', 'v2_m_mid_01', 'v2_m_senior_01', 'v2_f_young_01', 'v2_f_senior_01'
11
11
  ]);
12
12
  const TTS_VOICE_ID_SET = new Set(TTS_VOICE_IDS);
13
13
  const AUTH_PROVIDERS = Object.freeze([
@@ -175,6 +175,27 @@ function dataRequestInput(input) {
175
175
  };
176
176
  }
177
177
 
178
+ const SKILL_CATEGORIES = Object.freeze(['data', 'ai', 'dev', 'document', 'marketing', 'finance', 'productivity', 'video', 'etc']);
179
+ const SKILL_CATEGORY_SET = new Set(SKILL_CATEGORIES);
180
+
181
+ function normalizeSkillId(value) {
182
+ return encodeURIComponent(requiredString('skillId', value, 80));
183
+ }
184
+
185
+ function normalizeSkillRunId(value) {
186
+ return encodeURIComponent(requiredString('runId', value, 64));
187
+ }
188
+
189
+ function skillInput(value) {
190
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new TypeError('input must be a plain object matching the Skill input_schema.');
191
+ return value;
192
+ }
193
+
194
+ function optionalSkillText(name, value, maxLength) {
195
+ if (value === undefined || value === null || value === '') return undefined;
196
+ return requiredString(name, String(value), maxLength);
197
+ }
198
+
178
199
  function numberOrNull(value) {
179
200
  if (value === undefined || value === null || value === '') return null;
180
201
  const number = Number(value);
@@ -268,6 +289,7 @@ class ApickApiError extends Error {
268
289
  this.status = options && options.status || 0;
269
290
  this.code = options && options.code || 'APICK_API_ERROR';
270
291
  this.serviceCode = options && options.serviceCode || undefined;
292
+ if (options && options.details !== undefined) this.details = options.details;
271
293
  }
272
294
 
273
295
  toJSON() {
@@ -847,6 +869,128 @@ class ApickClient {
847
869
  getTaxReturnHistory(transactionId) {
848
870
  return this._call('requestTaxReturnHistory', { transactionId: normalizeTransactionId(transactionId) }, null, { endpoint: '/rest/get_tax_return_history' });
849
871
  }
872
+
873
+ // Skills: 요청·응답이 모두 JSON 이고 data·api 봉투 없이 응답 항목이 최상위에 온다.
874
+ // 오류는 { error: { code, message, details } } 형식이며 code 를 serviceCode 로 돌려준다.
875
+ async _skills(method, path, options) {
876
+ const config = options || {};
877
+ const controller = new AbortController();
878
+ const timeoutMs = this.#timeoutMs || config.timeoutMs || DEFAULT_TIMEOUT_MS;
879
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
880
+ const headers = { Accept: 'application/json', Authorization: `Bearer ${this.#apiKey}` };
881
+ const request = { method, headers, signal: controller.signal, redirect: 'error' };
882
+ if (config.idempotencyKey) headers['Idempotency-Key'] = config.idempotencyKey;
883
+ if (method !== 'GET') {
884
+ headers['Content-Type'] = 'application/json';
885
+ request.body = JSON.stringify(config.body || {});
886
+ }
887
+ let url = this.#baseUrl + path;
888
+ const query = new URLSearchParams(config.query || {}).toString();
889
+ if (query) url += '?' + query;
890
+
891
+ let response;
892
+ try {
893
+ response = await this.#fetch(url, request);
894
+ } catch (error) {
895
+ const timedOut = controller.signal.aborted || error && error.name === 'AbortError';
896
+ throw new ApickApiError(
897
+ timedOut ? `APICK request timed out after ${timeoutMs} ms.` : redact(error && error.message || 'APICK network request failed.', this.#apiKey),
898
+ { code: timedOut ? 'APICK_TIMEOUT' : 'APICK_NETWORK_ERROR' }
899
+ );
900
+ } finally {
901
+ clearTimeout(timer);
902
+ }
903
+
904
+ const raw = await response.text();
905
+ let body;
906
+ try {
907
+ body = raw ? JSON.parse(raw) : null;
908
+ } catch {
909
+ body = undefined;
910
+ }
911
+ const failure = body && typeof body === 'object' && body.error && typeof body.error === 'object' ? body.error : null;
912
+ if (!response.ok || failure) {
913
+ throw new ApickApiError(failure && typeof failure.message === 'string' && failure.message || publicErrorMessage(body, 'APICK request failed.'), {
914
+ status: response.status,
915
+ code: response.status === 401 ? 'APICK_AUTH_ERROR' : 'APICK_API_ERROR',
916
+ serviceCode: failure && typeof failure.code === 'string' ? failure.code : undefined,
917
+ details: failure && failure.details && typeof failure.details === 'object' ? failure.details : undefined
918
+ });
919
+ }
920
+ if (!body || typeof body !== 'object') {
921
+ throw new ApickApiError(`APICK returned an unexpected response (HTTP ${response.status}).`, {
922
+ status: response.status,
923
+ code: 'APICK_INVALID_RESPONSE'
924
+ });
925
+ }
926
+ return Object.freeze({ data: body, meta: responseMeta(null, response.headers) });
927
+ }
928
+
929
+ searchSkills(options) {
930
+ const config = options || {};
931
+ const query = {};
932
+ if (config.query !== undefined && config.query !== null && config.query !== '') query.query = requiredString('query', config.query, 60);
933
+ if (config.category !== undefined && config.category !== null && config.category !== '') {
934
+ const category = requiredString('category', config.category);
935
+ if (!SKILL_CATEGORY_SET.has(category)) throw new RangeError('category must be one of the supported Skill categories.');
936
+ query.category = category;
937
+ }
938
+ if (config.cursor !== undefined && config.cursor !== null && config.cursor !== '') query.cursor = requiredString('cursor', String(config.cursor), 20);
939
+ const limit = optionalRangeInteger('limit', config.limit, 1, 20);
940
+ if (limit !== undefined) query.limit = String(limit);
941
+ return this._skills('GET', '/rest/skills', { query });
942
+ }
943
+
944
+ getSkill(skillId) {
945
+ return this._skills('GET', '/rest/skills/' + normalizeSkillId(skillId));
946
+ }
947
+
948
+ quoteSkill(skillId, input, options) {
949
+ const body = { input: skillInput(input) };
950
+ const version = optionalSkillText('version', (options || {}).version, 16);
951
+ if (version !== undefined) body.version = version;
952
+ return this._skills('POST', '/rest/skills/' + normalizeSkillId(skillId) + '/quotes', { body });
953
+ }
954
+
955
+ // 같은 idempotencyKey 로 다시 보내면 새로 실행하지 않고 처음 실행을 돌려준다. 응답을 못 받았을 때는 같은 키로 재시도한다.
956
+ runSkill(skillId, input, options) {
957
+ const config = options || {};
958
+ const idempotencyKey = requiredString('idempotencyKey', config.idempotencyKey);
959
+ if (!/^[A-Za-z0-9._:-]{1,128}$/.test(idempotencyKey)) {
960
+ throw new TypeError('idempotencyKey must be 1-128 characters of letters, digits, and . _ : -');
961
+ }
962
+ const body = { input: skillInput(input) };
963
+ const version = optionalSkillText('version', config.version, 16);
964
+ const quoteId = optionalSkillText('quoteId', config.quoteId, 40);
965
+ const maxCostPoints = config.maxCostPoints === undefined || config.maxCostPoints === null ? undefined : positiveInteger('maxCostPoints', config.maxCostPoints);
966
+ const waitSeconds = optionalRangeInteger('waitSeconds', config.waitSeconds, 0, 20);
967
+ if (version !== undefined) body.version = version;
968
+ if (quoteId !== undefined) body.quote_id = quoteId;
969
+ if (maxCostPoints !== undefined) body.max_cost_points = maxCostPoints;
970
+ if (waitSeconds !== undefined) body.wait_seconds = waitSeconds;
971
+ return this._skills('POST', '/rest/skills/' + normalizeSkillId(skillId) + '/runs', { body, idempotencyKey, timeoutMs: 40_000 });
972
+ }
973
+
974
+ getSkillRun(runId) {
975
+ return this._skills('GET', '/rest/skills/runs/' + normalizeSkillRunId(runId));
976
+ }
977
+
978
+ getSkillRunResult(runId) {
979
+ return this._skills('GET', '/rest/skills/runs/' + normalizeSkillRunId(runId) + '/result');
980
+ }
981
+
982
+ cancelSkillRun(runId) {
983
+ return this._skills('POST', '/rest/skills/runs/' + normalizeSkillRunId(runId) + '/cancel', { body: {} });
984
+ }
985
+
986
+ skillUsage(options) {
987
+ const config = options || {};
988
+ const query = {};
989
+ if (config.cursor !== undefined && config.cursor !== null && config.cursor !== '') query.cursor = requiredString('cursor', String(config.cursor), 20);
990
+ const limit = optionalRangeInteger('limit', config.limit, 1, 50);
991
+ if (limit !== undefined) query.limit = String(limit);
992
+ return this._skills('GET', '/rest/skills/usage', { query });
993
+ }
850
994
  }
851
995
 
852
996
  module.exports = {
@@ -856,5 +1000,6 @@ module.exports = {
856
1000
  SERVICES,
857
1001
  TTS_VOICE_IDS,
858
1002
  AUTH_PROVIDERS,
1003
+ SKILL_CATEGORIES,
859
1004
  DEFAULT_BASE_URL
860
1005
  };
package/src/index.d.ts CHANGED
@@ -200,7 +200,7 @@ export interface YoutubeSubtitleList {
200
200
  export interface YoutubeSubtitleOptions { format?: 'vtt' | 'srt' | 'txt'; type?: 'any' | 'manual' | 'auto'; }
201
201
 
202
202
  export const TTS_VOICE_IDS: readonly [
203
- 'v2_ann_m_30s_01', 'v2_ann_m_30s_02', 'v2_ann_m_30s_04', 'v2_ann_m_30s_05', 'v2_ann_f_30s_01', 'v2_ann_f_30s_02', 'v2_ann_f_30s_03', 'v2_ann_f_30s_04', 'v2_ann_f_30s_05', 'v2_m_teen_01', 'v2_m_young_01', 'v2_m_mid_01', 'v2_m_senior_01', 'v2_f_teen_01', 'v2_f_young_01', 'v2_f_senior_01'
203
+ 'v2_ann_m_30s_01', 'v2_ann_m_30s_02', 'v2_ann_m_30s_04', 'v2_ann_m_30s_05', 'v2_ann_f_30s_02', 'v2_ann_f_30s_03', 'v2_ann_f_30s_04', 'v2_ann_f_30s_05', 'v2_m_teen_01', 'v2_m_young_01', 'v2_m_mid_01', 'v2_m_senior_01', 'v2_f_young_01', 'v2_f_senior_01'
204
204
  ];
205
205
  export type TtsVoiceId = typeof TTS_VOICE_IDS[number];
206
206
 
@@ -232,6 +232,8 @@ export class ApickApiError extends Error {
232
232
  readonly status: number;
233
233
  readonly code: string;
234
234
  readonly serviceCode?: string;
235
+ /** Skills 오류의 부가 정보. 예: INVALID_INPUT 의 `errors` 목록 */
236
+ readonly details?: Record<string, unknown>;
235
237
  toJSON(): {
236
238
  name: string;
237
239
  message: string;
@@ -321,6 +323,78 @@ export class ApickClient {
321
323
  getCashReceiptDeduction(transactionId: string): Promise<ApickResult<DataRequestResult<CashReceiptDeductionResultPayload>>>;
322
324
  requestTaxReturnHistory(input: RequestTaxReturnHistoryInput): Promise<ApickResult<DataRequestAcceptedData>>;
323
325
  getTaxReturnHistory(transactionId: string): Promise<ApickResult<DataRequestResult<TaxReturnHistoryResultPayload>>>;
326
+ searchSkills(options?: SkillSearchOptions): Promise<ApickResult<SkillPage<SkillSummary>>>;
327
+ getSkill(skillId: string): Promise<ApickResult<SkillDetail>>;
328
+ quoteSkill(skillId: string, input: Record<string, unknown>, options?: { version?: string }): Promise<ApickResult<SkillQuote>>;
329
+ runSkill<T = Record<string, unknown>>(skillId: string, input: Record<string, unknown>, options: SkillRunOptions): Promise<ApickResult<SkillRun<T>>>;
330
+ getSkillRun<T = Record<string, unknown>>(runId: string): Promise<ApickResult<SkillRun<T>>>;
331
+ getSkillRunResult<T = Record<string, unknown>>(runId: string): Promise<ApickResult<SkillRunResult<T>>>;
332
+ cancelSkillRun(runId: string): Promise<ApickResult<SkillRun>>;
333
+ skillUsage(options?: { cursor?: string; limit?: number }): Promise<ApickResult<SkillPage<SkillRun>>>;
334
+ }
335
+
336
+ export const SKILL_CATEGORIES: readonly ['data', 'ai', 'dev', 'document', 'marketing', 'finance', 'productivity', 'video', 'etc'];
337
+ export type SkillCategory = typeof SKILL_CATEGORIES[number];
338
+ export type SkillRunStatus = 'queued' | 'running' | 'completing' | 'succeeded' | 'failed' | 'timed_out' | 'cancelled';
339
+ export type SkillBillingStatus = 'reserved' | 'captured' | 'released' | 'partially_refunded' | 'refunded';
340
+ export type SkillFailureCode = 'EXECUTION_FAILED' | 'OUTPUT_INVALID' | 'TIMED_OUT' | 'CANCELLED' | 'UPSTREAM_UNAVAILABLE';
341
+ /** `ApickApiError.serviceCode` 로 돌아오는 Skills 오류 코드 */
342
+ export type SkillErrorCode =
343
+ | 'INVALID_INPUT' | 'AUTH_REQUIRED' | 'NOT_ALLOWED' | 'PAYMENT_REQUIRED' | 'SKILL_OR_RUN_NOT_FOUND'
344
+ | 'IDEMPOTENCY_KEY_REQUIRED' | 'IDEMPOTENCY_CONFLICT' | 'QUOTE_EXPIRED' | 'VERSION_UNAVAILABLE'
345
+ | 'PRICE_EXCEEDS_LIMIT' | 'RUN_NOT_CANCELLABLE' | 'RESULT_NOT_READY' | 'RESULT_EXPIRED'
346
+ | 'INSUFFICIENT_POINTS' | 'RATE_LIMITED' | 'BUDGET_EXCEEDED' | 'TEMPORARILY_UNAVAILABLE';
347
+ export interface SkillSearchOptions { query?: string; category?: SkillCategory; cursor?: string; limit?: number; }
348
+ export interface SkillRunOptions {
349
+ /** 이 실행을 구분하는 고유 값. 재시도할 때 같은 값을 씁니다. 영문·숫자와 `. _ : -` 1~128자 */
350
+ idempotencyKey: string;
351
+ version?: string;
352
+ quoteId?: string;
353
+ /** 가격이 이 값보다 높으면 실행하지 않습니다 */
354
+ maxCostPoints?: number;
355
+ /** 결과를 기다릴 시간(0~20초, 기본 20) */
356
+ waitSeconds?: number;
357
+ }
358
+ export interface SkillPage<T> { items: T[]; next_cursor: string | null; }
359
+ export interface SkillLimits { max_input_chars: number; timeout_seconds: number; }
360
+ export interface SkillSummary {
361
+ skill_id: string; slug: string; title: string; summary: string;
362
+ category: SkillCategory; category_label: string; version: string;
363
+ /** 실행마다 같은 기본 금액 / base amount charged on every run */
364
+ price_points: number;
365
+ /** 사용량까지 더한 예상 금액 / estimated amount including usage */
366
+ estimated_points: number;
367
+ /** true 면 실행마다 실제 사용량만큼 금액이 달라집니다 / amount varies per run with actual usage */
368
+ usage_priced: boolean;
369
+ seller: { name: string }; uses_generative_ai: boolean;
370
+ }
371
+ export interface SkillDetail extends SkillSummary {
372
+ description: string; billing_rule: 'validated_result'; limits: SkillLimits;
373
+ input_schema: Record<string, unknown>; output_schema: Record<string, unknown>;
374
+ examples: unknown[]; stats: Record<string, unknown> | null; published_at: string | null;
375
+ }
376
+ export interface SkillQuote {
377
+ quote_id: string; skill_id: string; version: string; price_points: number;
378
+ /** 이 입력의 예상 금액 / estimated amount for this input */
379
+ estimated_points: number;
380
+ /** 예약되는 최대 금액. 실제 차감액은 이 값을 넘지 않습니다 / maximum amount reserved; the charge never exceeds it */
381
+ max_points: number;
382
+ usage_priced: boolean;
383
+ expires_at: string; limits: SkillLimits; billing_rule: 'validated_result';
384
+ }
385
+ export interface SkillBilling { status: SkillBillingStatus; reserved_points: number; charged_points: number; refunded_points: number; }
386
+ export interface SkillRun<T = Record<string, unknown>> {
387
+ run_id: string; status: SkillRunStatus;
388
+ skill: { id: string; version: string; title?: string };
389
+ billing: SkillBilling; created_at: string | null; completed_at: string | null;
390
+ links: { self: string; result?: string };
391
+ failure_code?: SkillFailureCode; result_expires_at?: string | null;
392
+ /** status 가 succeeded 일 때만 */
393
+ result?: T;
394
+ }
395
+ export interface SkillRunResult<T = Record<string, unknown>> {
396
+ run_id: string; skill: { id: string; version: string; title?: string };
397
+ result: T; result_expires_at: string | null;
324
398
  }
325
399
 
326
400
  export default ApickClient;
package/src/index.js CHANGED
@@ -1,13 +1,14 @@
1
- import sdk from './index.cjs';
2
-
3
- export const {
4
- ApickClient,
5
- ApickApiError,
6
- ApickBinaryResult,
7
- SERVICES,
8
- TTS_VOICE_IDS,
9
- AUTH_PROVIDERS,
10
- DEFAULT_BASE_URL
11
- } = sdk;
12
-
13
- export default ApickClient;
1
+ import sdk from './index.cjs';
2
+
3
+ export const {
4
+ ApickClient,
5
+ ApickApiError,
6
+ ApickBinaryResult,
7
+ SERVICES,
8
+ TTS_VOICE_IDS,
9
+ AUTH_PROVIDERS,
10
+ SKILL_CATEGORIES,
11
+ DEFAULT_BASE_URL
12
+ } = sdk;
13
+
14
+ export default ApickClient;