태스크
개요
태스크는 ChatGPT·Claude 같은 AI가 여러분의 서버(워커)에서 일을 실행하게 하는 기능이에요. "우리 ERP에 A-1042 발주 넣어줘"라고 말하면, AI가 워커에 작업을 보내고 결과를 받아 대화에 보여줍니다.
- 3Min API는 작업을 전달하고 상태를 기록할 뿐, 작업 자체는 워커가 실행합니다
- 워커는 여러분이 직접 운영하는 HTTP 서버입니다
언제 쓰나요
- 사내 시스템에 일을 시킬 때 — ERP 발주 등록, 재고 조회, 사내 DB 검색
- 시간이 걸리는 작업 — 보고서 생성, 대량 변환 (최대 24시간)
- 내 PC에서만 돌아가는 스크립트 — 터널로 공개 URL을 붙이면 됩니다 (아래 참고)
데이터를 저장하고 모으는 것이 목적이라면 엔드포인트가 맞아요.
워커 코드를 직접 짜기 어렵다면, 쓰고 있는 AI에게 "3minapi 태스크 워커 만들어줘"라고 요청해 보세요. AI는
help도구에서 이 문서와 같은 계약을 읽고 코드를 작성합니다.
진입 경로
- 워커 등록: 대시보드 → 태스크 → 워커 탭 →
워커 등록 - 실행 확인: 대시보드 → 태스크 → 실행 기록 탭
동작 방식
① 웹에서 워커 등록 (한 번만)
② AI → task_create 태스크 생성, 상태 working
③ 3Min API → 워커 POST 서명된 요청 전달 (callback_url 포함)
④ 워커 → 즉시 2xx 응답 "받았다"는 뜻일 뿐, 결과가 아님
⑤ 워커 → callback_url 진행 메시지(선택) → completed 또는 failed
⑥ AI → task_get 결과 확인
| 어디서 | 할 일 |
|---|---|
| 웹 대시보드 | 워커 등록·수정·삭제, 시크릿 확인·교체, 실행 기록 조회 |
| AI 대화 | 워커 목록 확인, 태스크 보내기, 상태·결과 확인, 취소 |
| 워커 서버 | 요청 받기, 작업 처리, 결과 회신 |
AI는 워커의 URL과 시크릿을 볼 수 없습니다. 둘 다 웹에서만 다룹니다.
워커 등록 (웹)
| 항목 | 설명 | 제한 |
|---|---|---|
| 이름 | 필수. AI가 부르는 이름. 대소문자 구분 없이 고유, 등록 후 변경 불가 | 50자 |
| 설명 | 필수. AI가 이 설명을 보고 워커를 고릅니다 | 500자 |
| 워커 URL | 필수. 태스크를 POST로 받을 주소 | 2,048바이트 |
| 인증 헤더 / 인증 값 | 선택. 워커 자체 인증용. 값만 넣으면 헤더는 Authorization |
128 / 8,192바이트 |
- 설명은 구체적으로 쓰세요. AI는 URL이 아니라 이름과 설명만 봅니다.
erp-1보다 "우리 ERP에 발주를 등록하고 발주 번호를 돌려줍니다"처럼 무엇을 받아 무엇을 돌려주는지 적어야 AI가 골라요
시크릿 두 개 — 방향이 반대입니다
등록하면 시크릿 2개가 자동 발급됩니다. 워커 상세 화면(대시보드 → 태스크 → 워커 탭 → 워커 선택)에서 시크릿 보기·시크릿 복사로 확인하세요.
| 시크릿 | 형식 | 방향 | 용도 |
|---|---|---|---|
| 서명 시크릿 | whsec_... |
3Min API → 워커 | 들어온 요청이 3Min API가 보낸 것인지 검증 |
| 회신 키 | tm_task_... |
워커 → 3Min API | 회신할 때 Authorization: Bearer로 전송 |
- 두 값은 서로 다르고 바꿔 쓸 수 없습니다. 엔드포인트 API 키(
tm_live_·tm_test_)도 회신 키로 쓸 수 없어요
| 교체 | 동작 | 워커가 할 일 |
|---|---|---|
서명 시크릿 회전 |
48시간 동안 새 태스크에는 새·이전 서명이 함께 붙음. 회전 전 태스크의 재시도(최대 약 5시간)는 이전 서명만 | 이전 시크릿을 6시간 이상 유지한 뒤, 48시간 안에 교체 |
회신 키 재발급 |
이전 키 즉시 무효 (진행 중인 태스크 포함) | 바로 교체 |
48시간 안에 한 번 더 회전하면 처음 시크릿은 즉시 무효가 됩니다. 대시보드에는 현재 시크릿만 표시되므로, 이전 시크릿을 유지하려면 회전하기 전에 복사해 두세요.
워커 구현
워커가 지켜야 할 다섯 가지
- 2xx를 15초 안에 돌려주세요. 작업은 응답한 뒤에 처리합니다
- 작업이 실패해도 요청은 2xx로 받으세요. 실패는 회신으로 알립니다 (5xx는 재시도, 그 밖의 4xx는 즉시 실패 — 응답 코드)
task_id로 중복을 걸러내세요. 같은 태스크가 두 번 올 수 있습니다 (at-least-once)- 서명 검증은 선택이지만 권장합니다. 안 하면 워커 URL을 아는 누구나 가짜 태스크를 보낼 수 있어요 (서명 검증)
- 결과는
callback_url로 회신하세요.completed또는failed를 한 번 보냅니다. 회신이 없으면 24시간 뒤failed(결과 회신 API)
전달 요청
3Min API가 워커 URL로 보내는 요청입니다.
POST https://worker.example.com/tasks
Content-Type: application/json
webhook-id: 0199a1b2-...
webhook-timestamp: 1757650867
webhook-signature: v1,K3XkZ1n0...
X-3minapi-Task-Id: 0199a1b2-...
X-3minapi-Delivery-Attempt: 1
Authorization: Bearer xxx
| 헤더 | 내용 |
|---|---|
webhook-id |
태스크 ID (task_id와 같은 값) |
webhook-timestamp |
보낸 시각(Unix 초). 시도마다 새로 찍힘 |
webhook-signature |
서명. 회전 중에는 공백으로 구분된 두 개 (서명 검증) |
X-3minapi-Task-Id |
태스크 ID (webhook-id와 같은 값) |
X-3minapi-Delivery-Attempt |
몇 번째 전달 시도인지 (1부터) |
| 등록한 인증 헤더 | 워커 등록 때 인증 헤더·값을 넣은 경우에만 |
본문:
{
"type": "task.created",
"timestamp": "2026-09-12T04:21:07.123Z",
"data": {
"task_id": "0199a1b2-...",
"input": { "sku": "A-1042", "qty": 30 },
"callback_url": "https://api.3minapi.com/api/v1/tasks/0199a1b2-.../result"
}
}
| 필드 | 내용 |
|---|---|
task_id |
태스크 ID. 중복 제거에 사용 |
input |
AI가 보낸 JSON 객체 (값은 같지만 키 순서는 달라질 수 있음) |
callback_url |
이 태스크의 회신 주소. 상태·결과는 이 주소로 보냅니다 |
워커의 응답 코드
응답 본문은 읽지 않고 상태 코드만 봅니다.
| 응답 | 결과 |
|---|---|
| 2xx | 접수 완료. 회신이 올 때까지 working |
| 5xx(501·505 제외), 429, 15초 타임아웃, 연결 오류 | 재시도 — 30초·2분·10분·1시간·4시간 뒤, 총 6회(약 5시간). 429의 Retry-After는 더 길 때만 반영 |
| 그 밖의 4xx(401·404·409 등), 501, 505 | 재시도 없이 즉시 failed |
- 서명 검증에 실패하면 401을 돌려주세요. 시크릿을 잘못 넣었다면 첫 태스크부터 바로
failed가 되어 설정 문제를 빨리 알 수 있어요
서명 검증 (선택)
webhook-signature 헤더가 서명입니다. 3Min API가 워커의 서명 시크릿으로 계산해 붙이고, 워커는 같은 계산으로 일치하는지 확인합니다.
서명 대상 = "{webhook-id}.{webhook-timestamp}.{요청 본문 원본}"
키 = 서명 시크릿에서 whsec_ 를 떼고 base64 디코드한 바이트
서명 = "v1," + base64( HMAC-SHA256(키, 서명 대상) )
- 공식 Standard Webhooks 라이브러리를 쓰면 몇 줄로 끝납니다 (예시 코드). 언어별 패키지와 직접 구현 방법은 웹훅 서명 검증 (같은 규격)
- 요청 본문 원본 바이트로 검증하세요. JSON으로 파싱한 뒤 다시 문자열로 만들면 반드시 실패합니다
webhook-timestamp가 5분 넘게 차이 나면 라이브러리가 거부합니다 (재사용 공격 차단)- 서명이 두 개 오면 하나라도 일치하면 통과입니다 (회전 규칙)
- 검증하지 않는다면 최소한
callback_url이https://api.3minapi.com/으로 시작하는지 확인하세요 — 회신 키를 그 주소로 보내기 때문입니다
결과 회신 API
전달 요청에 담겨 온 callback_url로 상태를 보냅니다. 진행 메시지는 여러 번, 완료·실패는 한 번입니다.
POST https://api.3minapi.com/api/v1/tasks/{task_id}/result
Authorization: Bearer tm_task_...
Content-Type: application/json
- 주소:
callback_url이 위 형식에 태스크 ID까지 채워져 옵니다. 직접 조립하지 말고 그대로 쓰세요 - 회신 키: 워커 상세 화면의 회신 키 →
시크릿 복사. 워커 서버의 환경변수 등에 넣어 두세요
진행 중 — 태스크는 working 그대로, AI가 task_get으로 메시지를 봅니다
{ "status": "working", "status_message": "2/3 재고 확인 중" }
완료
{
"status": "completed",
"result": { "order_id": "PO-20260912-001" },
"status_message": "발주 PO-20260912-001 등록 완료"
}
실패
{
"status": "failed",
"error": { "code": 1001, "message": "재고 부족", "data": { "available": 12 } }
}
| 필드 | 규칙 |
|---|---|
status |
working · completed · failed 중 하나 |
status_message |
working일 때 필수, 나머지는 선택. 256자 초과분은 잘림. failed에 없으면 error.message |
result |
completed일 때 필수. null이 아닌 아무 JSON |
error |
failed일 때 필수. code(정수) + message(문자열), data는 선택 |
- 본문 전체가 100KB 이하여야 합니다.
result·error는 잘리지 않으므로 넘으면 회신 전체가 413으로 거부됩니다 - 접수된 회신 1회 = API 호출 1회. 진행 메시지는 의미 있는 단계에서만 보내세요
회신 API 응답 코드
| 코드 | 의미 | 워커의 대응 |
|---|---|---|
| 202 | 접수됨. 보통 몇 초 안에 반영 | — |
| 400 | 본문 형식 오류, 잘못된 태스크 ID, 문자열 안의 NUL 문자 | 코드 수정 |
| 401 | 회신 키 없음·틀림 (엔드포인트 API 키 포함) | 키 확인 |
| 404 | 이 워커의 태스크가 아님 | callback_url 확인 |
| 409 | 이미 끝난 태스크 (완료·실패·취소) | 회신 중단 |
| 413 | 본문 100KB 초과. 아무것도 기록되지 않음 | 태스크가 아직 working이면 줄여서 재전송 |
| 415 | Content-Type이 JSON이 아님 | 헤더 수정 |
| 503 | 일시 장애 | 잠시 후 재전송 |
예시 코드 (Node.js)
Node.js 18 이상, package.json에 "type": "module"이 필요합니다.
// npm i express standardwebhooks
import express from 'express';
import { Webhook } from 'standardwebhooks';
// 서명 시크릿(whsec_...)과 회신 키(tm_task_...)는 서로 다른 값입니다
const wh = new Webhook(process.env.TASK_SIGNING_SECRET.replace('whsec_', ''));
const CALLBACK_KEY = process.env.TASK_CALLBACK_KEY;
// 받은 task_id — 실서비스에서는 DB나 Redis에 저장하세요
const seen = new Set();
const app = express();
// 서명은 원문 바이트로 계산됩니다. JSON으로 파싱하면 검증이 실패해요.
app.post('/tasks', express.raw({ type: 'application/json' }), (req, res) => {
let envelope;
try {
envelope = wh.verify(req.body, {
'webhook-id': req.header('webhook-id'),
'webhook-timestamp': req.header('webhook-timestamp'),
'webhook-signature': req.header('webhook-signature')
});
} catch {
return res.sendStatus(401);
}
const { task_id, input, callback_url } = envelope.data;
// 규칙 3 — 이미 받은 태스크면 다시 처리하지 않음
if (seen.has(task_id)) return res.sendStatus(202);
seen.add(task_id);
// 규칙 1 — 먼저 응답하고, 작업은 그 뒤에
res.sendStatus(202);
run(input, callback_url);
});
async function run(input, callbackUrl) {
try {
// 진행 메시지 — 태스크는 working 그대로, AI가 task_get으로 봅니다
await report(callbackUrl, { status: 'working', status_message: '1/2 재고 확인 중' });
await checkStock(input); // ← 여러분의 실제 작업
await report(callbackUrl, { status: 'working', status_message: '2/2 발주 등록 중' });
const orderId = await createOrder(input);
// 완료 — 결과와 요약 메시지
await report(callbackUrl, {
status: 'completed',
result: { order_id: orderId },
status_message: `발주 ${orderId} 등록 완료`
});
} catch (err) {
// 규칙 2 — 작업이나 회신이 실패하면 실패로 회신
await report(callbackUrl, {
status: 'failed',
error: { code: 1001, message: String(err?.message ?? err) },
status_message: '발주 등록 실패'
}).catch(console.error);
}
}
// callbackUrl — 전달 요청의 callback_url을 그대로 사용
async function report(callbackUrl, body) {
for (let attempt = 1; attempt <= 3; attempt++) {
const res = await fetch(callbackUrl, {
method: 'POST',
headers: {
Authorization: `Bearer ${CALLBACK_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
}).catch(() => null);
// 202 접수, 409 이미 끝난 태스크 — 더 보낼 필요 없음
if (res?.status === 202 || res?.status === 409) return;
// 400·401·404·413은 다시 보내도 같은 결과
if (res && res.status !== 503) throw new Error(`report rejected: HTTP ${res.status}`);
// 503·네트워크 오류만 잠시 후 재시도
if (attempt < 3) await new Promise((r) => setTimeout(r, 1000 * 2 ** attempt));
}
throw new Error('report failed: service unavailable');
}
app.listen(8080);
로컬 PC에서 워커 실행하기
워커에는 인터넷에서 닿는 공개 URL이 필요합니다. 공유기 뒤의 PC라면 터널로 로컬 포트에 공개 HTTPS 주소를 붙이세요.
cloudflared tunnel --url http://localhost:8080
ngrok http 8080
- 설치와 사용법: Cloudflare Quick Tunnels · ngrok 시작하기
- 출력된 주소에 경로를 붙여(예:
https://xxxx.trycloudflare.com/tasks) 워커 URL로 등록합니다 - 계정 없는 임시 주소는 실행할 때마다 바뀝니다. 그때마다 워커 URL을 수정하거나 각 서비스의 고정 주소 기능을 쓰세요
- PC가 꺼져 있거나 터널이 끊기면 대부분 재시도(약 5시간) 후
failed입니다. 꺼진 주소에 404를 돌려주는 서비스라면 바로failed가 됩니다
태스크 호출 (AI)
워커를 등록하면 AI 대화에서 바로 쓸 수 있어요. 도구 이름은 몰라도 됩니다.
- "ERP에 A-1042 30개 발주 넣어줘"
- "아까 보낸 발주 어떻게 됐어?"
- "방금 보낸 태스크 취소해줘"
| 도구 | 하는 일 |
|---|---|
worker_list |
등록된 워커의 이름과 설명 (URL·시크릿은 보이지 않음) |
task_create |
워커 이름과 input(JSON 객체, 100KB 이하)으로 태스크 생성 |
task_get |
태스크 한 건의 상태, 메시지, 결과 또는 에러 |
task_list |
최근 태스크 목록 (결과 본문 제외) |
task_cancel |
진행 중인 태스크 취소 (최대 100건) |
- 같은 워커·같은
input을 2분 안에(타이밍에 따라 최대 약 4분) 다시 보내면 기존 태스크를 돌려줍니다. 완료·취소된 태스크도 그대로 돌려주고, 실패한 태스크만 새로 만듭니다 - 취소는 워커에 알리지 않습니다. 남은 전달 시도만 멈추고, 이미 받은 워커의 이후 회신은 409를 받습니다
상태 확인
| 상태 | 의미 | 바뀌는가 |
|---|---|---|
working |
전달 중이거나 워커가 처리 중 | 바뀜 |
completed |
워커가 완료를 회신 | 최종 |
failed |
워커의 실패 회신, 전달 실패, 또는 24시간 초과 | 최종 |
cancelled |
취소됨 | 최종 |
failed의 원인은 status_message에 남습니다.
| 원인 | status_message 예 |
|---|---|
| 워커가 실패 회신 | 워커가 보낸 메시지 (없으면 error.message) |
| 재시도 소진 | The task could not be delivered to the worker after 6 attempts (no response within 15s). |
| 즉시 실패 응답 | The task could not be delivered to the worker after 1 attempt (HTTP 401). |
| 24시간 초과 | Worker did not respond within 24 hours. |
확인하는 곳:
- AI —
task_get·task_list. 워커의result는 요약 없이 그대로 전달됩니다 - 웹 — 실행 기록 탭. 기간(7·30·60일, Free는 7일)과 상태로 걸러 보고, 행을 누르면 입력·결과·에러·배달을 봅니다. 전달이 최종 실패했다면 배달에 시도 횟수·마지막 응답·사유가 표시됩니다
제약사항
| 항목 | 값 |
|---|---|
| 워커 개수 | Free 1개 / 유료 무제한 |
| 태스크 개수·동시 실행 | 무제한 (월 API 호출 한도 안에서) |
| 작업 기한 | 생성 후 24시간 — 넘으면 failed (매시 점검이라 최대 약 25시간) |
| 이력 보존 | 유료 최소 60일 / Free는 워커 등록 7일 후 워커와 실행 기록 모두 삭제 |
input 크기 |
100KB |
| 회신 본문 | 100KB (status_message는 256자에서 절단) |
| 전달 타임아웃 · 재시도 | 15초 · 총 6회, 약 5시간 |
| 서명 시크릿 회전 유예 | 48시간 |
| 회신 키 재발급 | 즉시 이전 키 무효 |
| API 호출 집계 | 성공한 태스크 도구 호출 1회·접수된 회신 1회 = 각 1콜. 전달·재시도는 0콜. 한도 초과여도 회신은 허용 |
| 전달 보장 | at-least-once (같은 태스크가 두 번 올 수 있음) |
개인정보 안내
- 워커의
result·error는 마스킹 없이 AI 대화에 그대로 전달됩니다 - 3Min API는 자유 형식 JSON에서 개인정보를 판별하지 않으니, 결과에 필요 이상의 개인정보를 담지 마세요
자주 막히는 부분
- 모든 태스크가 바로
failed예요: 실행 기록 상세의 배달 → 마지막 응답을 보세요. 401이면 서명 시크릿 자리에 회신 키를 넣었거나, 시크릿 회전 직후 이전 시크릿을 지웠을 수 있어요 - 태스크가 계속
working이에요: 워커가 2xx만 주고 회신을 안 보내는 경우입니다. 워커 로그에서 회신 API 응답 코드를 확인하세요 - 회신이 401이에요:
tm_task_로 시작하는 회신 키인지, 재발급 전의 키가 아닌지 확인하세요 - 회신이 409예요: 이미 끝났거나 취소된 태스크입니다. 더 보내지 않아도 됩니다
- 회신이 413이에요: 본문이 100KB를 넘었습니다.
result를 줄여 다시 보내세요 - 같은 요청인데 새 태스크가 안 생겨요: 2분 안의 같은
input은 기존 태스크를 돌려줍니다. 날짜 같은 값을input에 넣어 구분하세요