# Webhook

> 서명된 job.completed와 job.failed 이벤트를 내 서버에서 받고, Webhook 트리거 URL로 워크플로를 시작하고, 일정을 만들고, 결과를 보내는 방법입니다.

Source: https://nodaro.ai/ko/docs/developers/api/webhooks

**Webhook**은 Nodaro와 내 다른 시스템을 양방향으로 연결합니다. 내가 등록한 **Webhook 엔드포인트**는 내 작업 하나가 끝날 때마다 서명된 이벤트를 받으므로, 내 서버가 폴링할 필요가 없습니다. [Webhook 트리거](https://nodaro.ai/docs/nodes/automate/webhook-trigger) 노드는 어떤 시스템이든 호출해 실행을 시작할 수 있는 URL을 워크플로에 부여하고, [일정 트리거](https://nodaro.ai/docs/nodes/automate/schedule-trigger)는 정해진 시간표에 따라 워크플로를 실행하며, [Webhook 출력](https://nodaro.ai/docs/nodes/publish/webhook-output) 노드는 실행 결과를 내 서버로 보냅니다.

Workflow: 외부 시스템이 Webhook 트리거 URL을 호출하면 이미지 생성이 실행되고, Webhook 출력이 이미지 URL을 내 서버로 보냅니다.

- Webhook 트리거 → 이미지 생성 (프롬프트)
- 이미지 생성 → Webhook 출력

## 엔드포인트
이 라우트들은 Nodaro가 작업 이벤트를 보내는 엔드포인트를 관리합니다. [내 서버에서 작업 이벤트 받기](#job-events)를 참고하세요.

| 메서드 | 경로 | 기능 |
| --- | --- | --- |
| `POST` | `/v1/webhook-endpoints` | 엔드포인트를 등록합니다. 응답에 서명 시크릿이 한 번만 담깁니다. |
| `GET` | `/v1/webhook-endpoints` | 내 엔드포인트를 최신순으로 반환합니다. 시크릿은 절대 나열되지 않습니다. |
| `GET` | `/v1/webhook-endpoints/:id` | 엔드포인트 하나입니다. |
| `PATCH` | `/v1/webhook-endpoints/:id` | `url`, `events`, `description`, `isActive`를 바꿉니다. |
| `DELETE` | `/v1/webhook-endpoints/:id` | 엔드포인트와 그 전송 기록을 삭제합니다. |
| `POST` | `/v1/webhook-endpoints/:id/rotate-secret` | 서명 시크릿을 교체하며, 이전 시크릿에는 유예 기간을 줍니다. |
| `GET` | `/v1/webhook-endpoints/:id/deliveries` | 전송 기록을 최신순으로 반환합니다. |
| `GET` | `/v1/webhook-endpoints/:id/deliveries/:deliveryId` | 전송 하나입니다. |
| `POST` | `/v1/webhook-endpoints/:id/test` | 테스트 `ping`을 보냅니다. |

이 라우트들은 워크플로를 시작하고 그 트리거를 관리합니다. `/v1/webhook-endpoints` 아래에 있지 않습니다.

| 메서드 | 경로 | 기능 |
| --- | --- | --- |
| `POST` | `/v1/webhooks/:token` | 실행을 시작합니다. 공개 경로이며, 경로에 든 토큰이 자격 증명입니다. |
| `GET` | `/v1/workflows/:id/triggers` | 워크플로의 트리거를 각 Webhook의 URL, 토큰과 함께 반환합니다. |
| `PATCH` | `/v1/workflow-triggers/:id` | `isActive`로 트리거를 일시 중지하거나 재개하고, 일정의 `config`를 바꿉니다. |
| `POST` | `/v1/workflow-triggers` | 어떤 노드에도 묶이지 않은 트리거를 직접 만듭니다. |
| `POST` | `/v1/workflows/:id/sync-triggers` | 저장된 버전을 기준으로 워크플로의 트리거 노드를 다시 등록합니다. |

## 내 서버에서 작업 이벤트 받기
`https` 엔드포인트를 등록하면, 내 작업 하나가 끝날 때마다 Nodaro가 그 엔드포인트로 `POST`를 보냅니다. 내 서버는 `GET /v1/jobs/:id`를 폴링하지 않고도 생성이 끝났다는 것을 알 수 있습니다. 각 요청은 엔드포인트 자체의 시크릿으로 서명되고, 내 서버가 응답할 때까지 간격을 늘려 가며 재시도되며, 엔드포인트의 전송 기록에 남습니다.

### 엔드포인트 등록
**curl**

```bash
curl -s -X POST "https://app.nodaro.ai/v1/webhook-endpoints" \
  -H "Authorization: Bearer $NODARO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/nodaro-webhook", "description": "order fulfilment"}'
```

**TypeScript SDK**

```ts
const { data: endpoint, secret } = await client.webhooks.create({
url: "https://example.com/nodaro-webhook",
description: "order fulfilment",
})
// Store `secret` (whsec_...) with your server's config: it is shown only once.

await client.webhooks.test(endpoint.id) // a ping, sent within seconds
```

**CLI**

```bash
nodaro webhooks create --url https://example.com/nodaro-webhook
nodaro webhooks test <id> --wait
```

응답은 `{ data, secret }`과 함께 `201`입니다. 시크릿은 `whsec_`로 시작하고 이 응답에만 나타나므로, 바로 저장하세요. `events`는 선택 사항입니다. `events`에 둘 중 하나를 지정하지 않으면 엔드포인트는 두 작업 이벤트를 모두 받습니다.

엔드포인트는 다음과 같이 읽힙니다.

```json
{
"id": "3f6c2a1e-8b4d-4c7a-9e1f-2d5b6a7c8e90",
"url": "https://example.com/nodaro-webhook",
"description": "order fulfilment",
"events": ["job.completed", "job.failed"],
"isActive": true,
"disabledReason": null,
"disabledAt": null,
"consecutiveFailures": 0,
"appId": null,
"secretCreatedAt": "2026-10-15T10:00:00Z",
"previousSecretExpiresAt": null,
"createdAt": "2026-10-15T10:00:00Z",
"updatedAt": "2026-10-15T10:00:00Z"
}
```

### 이벤트
| 이벤트 | 보내는 때 |
| --- | --- |
| `job.completed` | 내 작업이 `completed`에 도달했을 때입니다. 검토를 위해 보류된 작업(`pending_review`)은 승인될 때 이벤트를 보냅니다. |
| `job.failed` | 내 작업이 `failed`에 도달했을 때입니다. |
| `ping` | `POST /v1/webhook-endpoints/:id/test`에서만 보냅니다. 엔드포인트는 이 이벤트를 구독하지 않습니다. |

내 작업 목록에 있는 모든 작업이 해당합니다. 단일 생성도, 워크플로 실행 안의 각 노드 작업도 포함됩니다. 취소된 작업은 아무것도 보내지 않으며, `pending`과 `processing` 같은 중간 상태도 마찬가지입니다. 실패했다가 나중에 완료된 작업은 끝날 때마다 이벤트를 하나씩 보냅니다. **컴포넌트** 노드 실행 안의 작업은 내 작업 목록에 없으므로 아무것도 보내지 않습니다. 실행을 시작한 노드가 대신 보고합니다.

### 요청
Nodaro는 `Content-Type: application/json`과 다음 헤더를 담아 `POST`를 보냅니다.

| 헤더 | 값 |
| --- | --- |
| `Nodaro-Signature` | `t=<unix seconds>,v1=<hex>`. 시크릿을 교체하는 동안에는 시크릿마다 `v1=`이 하나씩 붙습니다. |
| `Nodaro-Event` | `job.completed`, `job.failed`, `ping` |
| `Nodaro-Delivery` | 전송 ID이며, 이 이벤트의 모든 재시도에서 같습니다 |
| `Nodaro-Webhook` | 엔드포인트 ID |
| `Nodaro-Attempt` | 첫 시도는 `1`, 그다음은 `2`, `3` 등 |
| `User-Agent` | `Nodaro-Webhooks/1` |

서명되는 것은 타임스탬프와 본문뿐입니다. 라우팅과 중복 제거는 `Nodaro-Event`나 `Nodaro-Delivery` 헤더가 아니라, 검증된 본문의 `type`과 `id`로 하세요.

```json
{
"id": "6f1d0c0e-1c7e-4d0a-9f2b-0a4e2f7b9c11",
"type": "job.completed",
"createdAt": "2026-10-15T10:00:04.512Z",
"data": {
"job": {
"id": "0b9a5c1d-2e3f-4a5b-8c6d-7e8f9a0b1c2d",
"status": "completed",
"progress": 100,
"job_type": "generate-image",
"input_data": { "type": "generate-image", "prompt": "a lighthouse at dusk" },
"output_data": { "imageUrl": "https://cdn.nodaro.ai/images/0b9a5c1d.png" },
"error_message": null,
"error_hint": null,
"credit_status": "committed",
"source": "sdk",
"session_id": null,
"created_at": "2026-10-15T10:00:00.000Z",
"completed_at": "2026-10-15T10:00:04.512Z"
}
}
}
```

- **`id`**는 전송 ID입니다. 이벤트와 엔드포인트마다 고유하고 모든 재시도에서 같으므로, 이 값으로 중복을 제거하세요.
- **`createdAt`**은 작업이 이 상태에 도달한 시각입니다.
- **`data.job`**은 `GET /v1/jobs/:id`가 반환하는 형식의 작업이며, 첫 시도 시점의 상태입니다. 재시도는 바이트 하나까지 같은 본문을 보냅니다. 청구가 아직 정산되지 않았으면 `credits_charged`가 여전히 `null`이고 `credit_status: "reserved"`일 수 있습니다. 최종 수치는 작업을 다시 읽으세요.
- **이벤트는 최대 64KB입니다.** 더 큰 작업은 `input_data`부터, 그다음 `output_data`를 빼며, `data.omitted`에 뺀 항목이 나열됩니다. 작업 전체는 `GET /v1/jobs/:id`에 있습니다.
- **`ping`**에는 `"data": { "webhookId": "<endpoint id>" }`가 담깁니다.

### 서명 검증
`v1`은 엔드포인트의 시크릿 전체(`whsec_...`, UTF-8)를 키로 한 `<t>.<raw body>`의 HMAC-SHA256입니다. 요청을 신뢰하기 전에 세 가지를 확인하세요.

1. 서명이 받은 그대로의 바이트와 일치하는지 확인합니다. 먼저 JSON을 다시 직렬화하지 마세요.
2. 타임스탬프가 최근인지 확인합니다. 일반적인 허용 범위는 5분입니다. 이 확인은 가로챈 요청을 나중에 다시 보내는 것을 막습니다.
3. 이 `id`를 이미 처리하지 않았는지 확인합니다.

SDK에서는 `@nodaro/sdk`의 `verifyWebhookSignature`가 이 확인을 처리합니다. Web Crypto만 사용하므로 Node 20 이상, Deno, Bun, Cloudflare Workers, Vercel Edge, 브라우저에서 실행됩니다.

```ts

const app = express()

app.post("/nodaro-webhook", express.raw({ type: "application/json" }), async (req, res) => {
try {
const event = await verifyWebhookSignature({
payload: req.body, // the raw bytes
header: req.headers["nodaro-signature"],
secret: process.env.NODARO_WEBHOOK_SECRET!,
toleranceSeconds: 300,
})
if (event.type === "job.completed") {
await saveResult(event.data.job.id, event.data.job.output_data)
}
res.sendStatus(204) // any 2xx within 10 seconds counts as delivered
} catch (err) {
if (err instanceof WebhookSignatureError) return res.status(400).send(err.code)
throw err
}
})
```

`verifyWebhookSignature`는 파싱된 이벤트를 반환하거나 `WebhookSignatureError`를 발생시킵니다. 그 `code`는 `missing_header`, `malformed_header`, `signature_mismatch`, `timestamp_outside_tolerance`, `invalid_payload`, `missing_secret`, `crypto_unavailable` 중 하나입니다.

SDK 없이 Node에서는 다음과 같습니다.

```ts

// rawBody: the request body as received (a Buffer or a string).
function verify(rawBody: Buffer | string, header: string, secret: string, toleranceSeconds = 300) {
const pairs = header.split(",").map((part) => part.trim().split("=", 2))
const t = pairs.find(([key]) => key === "t")?.[1]
const expected = createHmac("sha256", secret).update(`${t}.`).update(rawBody).digest()
const matches = pairs.some(([key, value]) => {
if (key !== "v1") return false
const got = Buffer.from(value, "hex")
return got.length === expected.length && timingSafeEqual(got, expected)
})
return matches && Math.abs(Date.now() / 1000 - Number(t)) <= toleranceSeconds
}
```

교체의 유예 기간 동안에는 헤더에 시크릿마다 `v1=`이 하나씩, 새 시크릿부터 담깁니다. 그중 하나라도 일치하면 요청을 받아들이세요. 전송 기록은 성공하지 못한 전송의 정확한 본문(`requestBody`)을 보관하므로, 실패한 확인을 바이트 하나까지 재현할 수 있습니다.

### 재시도
10초 안에 받은 `2xx` 응답은 모두 전송된 것으로 봅니다. 그 밖의 경우는 시도가 실패한 것입니다. `3xx`(리디렉션은 따라가지 않음), `4xx`, `5xx`, 시간 초과, 응답하지 않는 주소입니다. 그러면 전송을 다시 시도합니다.

| 시도 후 | 다음 시도까지 |
| --- | --- |
| 1 | 30초 |
| 2 | 2분 |
| 3 | 10분 |
| 4 | 1시간 |
| 5 | 6시간 |
| 6 | 12시간 |
| 7 | 24시간 |

각 대기 시간은 위아래로 최대 10%까지 달라집니다. 8번째 시도, 곧 첫 시도로부터 약 43시간 뒤에는 전송이 최종 실패합니다. 테스트 `ping`은 한 번만 시도합니다.

- **전송은 최소 한 번입니다.** 예를 들어 내 서버가 너무 늦게 응답했을 때처럼, 서버가 첫 시도를 처리한 뒤에 재시도가 도착할 수 있습니다. 전송 `id`로 중복을 제거하세요.
- **이벤트는 순서가 뒤바뀌어 도착할 수 있습니다.** 한 엔드포인트에는 한 번에 최대 5개, 한 계정에는 10개의 전송이 갑니다. 도착 순서가 아니라 `createdAt`과 작업 자체의 `status`를 쓰세요.
- **전혀 응답하지 않는 서버는 엔드포인트를 쉬게 합니다.** HTTP 응답 없이 5번 연속 시도하면 엔드포인트는 1분간 쉬며, 다시 일어날 때마다 두 배씩 늘어나 최대 1시간까지 쉽니다. 대기 중인 전송은 시도 횟수를 유지하며, HTTP 응답이 하나라도 오면 휴식이 끝납니다.
- **Nodaro 쪽 문제는 내 시도 횟수로 세지 않습니다.** 전송은 몇 분 기다렸다가 시도 횟수를 쓰지 않고 다시 시도합니다. 그동안 기록 항목에는 `signing_unavailable` 또는 `job_unreadable`이 표시됩니다.

### 자동 꺼짐
전송 50개가 연속으로 최종 실패하면 Nodaro가 엔드포인트를 끕니다. `isActive`는 `false`가 되고 `disabledReason`은 `"consecutive_failures"`가 됩니다. 수신 측을 고치고 테스트 `ping`으로 확인한 다음, `PATCH /v1/webhook-endpoints/:id`에 `{ "isActive": true }`를 보내 엔드포인트를 켜세요. 그러면 횟수도 초기화되며, 성공이 하나라도 있어도 초기화됩니다.

엔드포인트가 꺼져 있는 동안에는, 내가 껐든 Nodaro가 껐든 이벤트가 기록되지 않습니다. 이미 대기 중인 전송은 `error: "endpoint_disabled"`와 함께 `failed`로 끝납니다. 테스트 `ping`은 그래도 보냅니다.

### 전송 기록
`GET /v1/webhook-endpoints/:id/deliveries`는 엔드포인트의 전송을 최신순으로, 기본 페이지당 20개, 최대 100개까지 나열합니다. `status`(`pending`, `delivered`, `failed`)로 필터링하고, `limit`과 `cursor`로 페이지를 넘기세요. 응답은 `{ data, next }`입니다. 기록은 30일 동안 보관됩니다.

각 전송에는 `event`, `jobId`, `status`, `attempts`, 수신 측의 마지막 `responseStatus`, `responseBody`의 처음 2KB, `durationMs`, `error`, 그리고 대기 중일 때는 `nextAttemptAt`이 담깁니다. `requestBody`는 보낸 그대로의 본문으로, 전송이 대기 중이거나 실패한 뒤에 보관되며, 전송되면 `null`이 됩니다. `GET /v1/webhook-endpoints/:id/deliveries/:deliveryId`는 테스트가 대기열에 넣은 `ping`처럼 전송 하나를 읽습니다.

`error`는 다음 값 중 하나입니다.

| `error` | 의미 |
| --- | --- |
| `http_<status>` | 내 서버가 그 상태로 응답했습니다. |
| `timeout` | 10초 안에 응답이 없었습니다. |
| `blocked_address` | 주소가 공개 인터넷 주소로 확인되지 않습니다. Nodaro가 연결하기 전에 거부했습니다. |
| `connection_refused`, `connection_reset`, `tls_error`, `network_error` | 연결이 실패했습니다. |
| `endpoint_disabled` | 전송이 대기하는 동안 엔드포인트가 꺼졌습니다. |
| `app_revoked` | 엔드포인트를 등록한 연결된 앱을 내가 해지했습니다. |
| `job_deleted` | 보내기 전에 작업이 삭제되었습니다. |
| `attempts_exhausted` | 마지막 시도가 응답이 기록되기 전에 Nodaro 쪽에서 끊겼습니다. |
| `signing_unavailable`, `job_unreadable` | Nodaro 쪽 문제입니다. 전송은 기다렸다가 시도 횟수를 쓰지 않고 다시 시도합니다. |

### 시크릿 교체
`POST /v1/webhook-endpoints/:id/rotate-secret`은 `{ secret, previousSecretExpiresAt }`로 응답하며, 새 시크릿은 이 응답에만 나타납니다. 그때부터 새 시크릿이 재시도를 포함한 모든 전송에 서명합니다. 교체된 시크릿은 `gracePeriodSeconds` 동안 함께 계속 서명합니다. 기본값은 하루이고 최대 7일입니다. 그동안 공백 없이 수신 측을 업데이트할 수 있습니다. 유출된 뒤처럼 이전 시크릿을 즉시 중단하려면 `{ "gracePeriodSeconds": 0 }`을 보내세요.

### 규칙과 한도
- **공개 호스트의 `https`만 가능합니다.** `http://`, `localhost`, 사설·루프백·링크 로컬 주소, 그리고 `.internal`이나 `.local`로 끝나는 이름처럼 사설 네트워크 안에서만 작동하는 이름은 안 됩니다. URL에 사용자 이름이나 비밀번호를 넣을 수 없습니다. 주소는 시도할 때마다 다시 확인됩니다. 내 컴퓨터에서 받으려면 터널을 쓰고 그 `https` 주소를 등록하세요.
- **주소마다 엔드포인트 하나.** 같은 주소를 두 번 등록하면 `409 url_taken`으로 응답합니다. 대신 엔드포인트 하나로 두 이벤트를 모두 구독하세요.
- **계정마다 엔드포인트 10개.** 11번째는 `400 limit_reached`로 응답합니다.
- **쓰기에는 요청 한도가 있습니다.** 만들기, 업데이트, 삭제, 시크릿 교체는 분당 30회, 테스트 ping은 분당 10회입니다.
- **엔드포인트를 관리할 수 있는 주체:** 로그인한 세션, 개인 API 토큰, `jobs:read` 범위가 있는 연결된 앱의 토큰입니다. 일부 워크플로로 제한된 개인 토큰은 관리할 수 없습니다.
- **연결된 앱이 등록한 엔드포인트**(`appId`가 설정됨)는 그 앱이 직접 제출한 작업만 받으며, 앱에 대한 내 승인에 `jobs:read`가 포함되어 있는 동안에만 받습니다. 앱이 시작한 워크플로 실행의 작업은 포함되지 않으므로, `GET /v1/workflow-executions/:id`로 실행을 추적하세요. 앱은 자신이 등록한 엔드포인트만 봅니다. 나는 모두 보고 삭제할 수 있습니다.
- **셀프 호스팅 설치**는 자체 서버에서 보내며 `NODARO_ENCRYPTION_KEY`가 필요합니다. Docker Compose 스택이 이를 생성합니다. 이 값이 없으면 라우트는 `503 encryption_key_missing`으로 응답합니다.
- **MCP에서는 사용할 수 없습니다.** 엔드포인트를 등록하면 서명 시크릿이 반환되고 계정 전체의 전송이 설정되므로, AI 에이전트가 아니라 앱이나 CLI가 할 일입니다.

그 밖의 오류: 잘못된 주소나 본문에는 `400 validation_error`, `403 insufficient_scope`, 내 것이 아닌 ID에는 `404 not_found`, 저장된 주소를 규칙이 더 이상 받아들이지 않는 엔드포인트에는 `409 url_invalid`입니다. 켜거나 테스트하기 전에 `url`을 바꾸세요. `409 secret_required`는 서명이 생기기 전에 등록된 엔드포인트라는 뜻입니다. 켜거나 테스트하기 전에 시크릿을 교체하세요.

## HTTP 호출로 워크플로 시작
### Webhook 트리거 추가
워크플로에 [Webhook 트리거](https://nodaro.ai/docs/nodes/automate/webhook-trigger) 노드를 추가하세요. **출력 파라미터**에서 호출하는 쪽이 보낼 값마다 파라미터를 하나씩 추가하세요. 각 파라미터에는 요청 JSON 본문의 키와 일치하는 **이름**과, `text`, `imageUrl`, `videoUrl`, `audioUrl` 중 하나인 **유형**이 있습니다. 파라미터를 그 값을 사용할 노드에 연결하세요.

### 워크플로 저장
저장하면 엔드포인트가 만들어집니다. Nodaro가 32바이트 토큰을 발급하고 `POST /v1/webhooks/<token>`을 등록합니다. 편집기, API, SDK, 가져오기, MCP 중 어떤 방법으로 저장하든 똑같이 동작합니다. 토큰은 한 번 발급된 뒤 유지되므로, 외부 시스템에 준 URL은 이후 저장해도 계속 유효합니다. 노드를 제거하면 URL도 폐기됩니다.

### URL 확인
`GET /v1/workflows/<id>/triggers`는 각 Webhook의 `webhookUrl`과 `webhookToken`을 포함해 워크플로의 트리거를 반환합니다. `webhookUrl`은 `/v1/webhooks/<token>` 형태의 경로이므로 앞에 내 Nodaro 주소를 붙이세요. `config.nodeId`가 노드의 ID인 트리거가 그 Webhook 트리거 노드의 트리거입니다. 목록에는 내가 소유한 워크플로의 트리거만 나옵니다.

편집기에서는 노드의 설정 패널에 있는 **Webhook URL**에 같은 주소가 전체로 표시되며, 복사 버튼도 있습니다.

### 호출
JSON 본문과 함께 `POST`를 보내세요. URL에 든 토큰이 자격 증명이므로 `Authorization` 헤더는 필요하지 않습니다.

```bash
curl -s -X POST "https://app.nodaro.ai/v1/webhooks/$WEBHOOK_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"prompt": "a lighthouse in a storm", "imageUrl": "https://example.com/lighthouse.jpg"}'
```

본문은 트리거의 페이로드가 됩니다. 각 파라미터는 같은 이름의 키를 읽고, 유형에 따라 값이 전달될 곳이 정해집니다. `imageUrl`은 이미지 입력으로, `videoUrl`은 동영상 입력으로 들어갑니다.

Webhook 트리거마다 분당 10개의 요청을 받습니다. 이 한도는 내 API 토큰의 한도와 별개입니다.

흔한 호출 주체로는 고객이 가입하면 온보딩 동영상을 시작하는 결제 시스템, 릴리스 노트 요약을 요청하는 코드 저장소, 새 기사를 게시하는 콘텐츠 시스템, 노코드 자동화 도구가 있습니다.

### 트리거된 실행이 실행하는 범위
- **무언가에 연결된 트리거는 자기 브랜치만 실행합니다.** 트리거의 다운스트림 노드와, 그 노드들이 입력으로 필요로 하는 모든 노드입니다.
- **아무것에도 연결되지 않은 트리거는 워크플로 전체를 실행합니다.**
- **따라서 워크플로 하나에 트리거를 여러 개 둘 수 있으며**, 각 트리거가 자기 브랜치를 시작합니다.
- **“연결”은 다른 노드에 값을 넣는 모든 것을 뜻합니다.** 직접 그린 연결, 그룹 안의 노드, 필드 매핑이 모두 해당됩니다.
- **편집기, API, 게시된 앱에서 시작한 실행은 트리거의 제한을 받지 않습니다.** 늘 실행하던 범위를 그대로 실행합니다.

API로 직접 만든 트리거는 어떤 노드도 지정하지 않습니다. 이때 Nodaro는 캔버스에 있는 유일한 Webhook 트리거를 사용하며, 두 개가 있으면 워크플로 전체가 실행됩니다.

### URL을 비밀로 유지
토큰이 유일한 자격 증명이므로 URL을 가진 사람은 누구나 실행을 시작하고 내 크레딧을 쓸 수 있습니다. 조직의 워크스페이스에 있는 워크플로라면 실행 비용은 워크스페이스의 예산에서 나갑니다. URL을 공유받은 사람은 누구나 클래스나 팀의 크레딧을 쓸 수 있습니다.

Webhook, 일정, Telegram 등 모든 자동 실행은 먼저 트리거를 만든 사람이 아직 워크플로를 실행할 수 있는지 확인합니다. 권한이 취소되었거나, 멤버십이 정지되었거나, 워크스페이스가 보관되어 더 이상 실행할 수 없으면 자동화가 멈춥니다. 그러면 워크플로의 실행 기록에 `run_requires_authenticated_member` 코드와 함께 실패 항목이 하나 표시됩니다.

## API로 트리거 관리
트리거 노드는 워크플로를 저장할 때 스스로 등록됩니다. 다음 네 경로로 트리거를 직접 관리합니다.

- <strong>`GET /v1/workflows/<id>/triggers`</strong>는 워크플로의 트리거 목록을 반환합니다.
- <strong>`PATCH /v1/workflow-triggers/<id>`</strong>에 `{ "isActive": false }`를 보내면 트리거가 일시 중지되고, `true`를 보내면 재개됩니다. 노드에 속한 트리거는 다음 저장 때 노드 자체의 스위치가 다시 적용되므로, 계속 유지해야 하는 설정은 노드의 스위치를 사용하세요. 보낸 `config`는 저장된 값에 병합되므로 바뀌는 부분만 보내세요. 트리거와 노드의 연결, 실행 횟수는 유지됩니다.
- <strong>`POST /v1/workflows/<id>/sync-triggers`</strong>는 저장된 버전을 기준으로 워크플로의 트리거 노드를 다시 등록합니다. 편집기는 저장할 때마다 이 경로를 호출하며, 편집 권한이 있는 사람은 누구나 호출할 수 있습니다. 응답은 `{ "data": { "synced": …, "created": …, "updated": …, "removed": … } }`입니다.
- <strong>`POST /v1/workflow-triggers`</strong>는 어떤 노드에도 속하지 않는 트리거를 만들므로, 워크플로를 저장해도 이 트리거는 바뀌거나 제거되지 않습니다. 트리거는 아무도 지켜보지 않는 실행이므로, 만들려면 워크플로를 실행할 때와 같은 권한이 필요합니다.

## 일정
[일정 트리거](https://nodaro.ai/docs/nodes/automate/schedule-trigger)는 정해진 시간표에 따라 워크플로를 실행합니다. 일정은 **규칙**의 목록이며, 현재 분에 맞는 규칙이 하나라도 있으면 워크플로가 실행됩니다.

### 일정 켜기
일정 트리거 노드는 데이터에 노드의 스위치인 `"active": true`가 있을 때만 실행됩니다. API, SDK, MCP로 작성한 노드에 `active`가 없으면 **일시 중지** 상태로 등록됩니다. 편집기의 스위치와 **일정** 버튼은 같은 필드를 설정합니다. 템플릿으로 내보내면 이 필드가 포함되지 않으므로, 가져온 일정은 항상 일시 중지 상태로 시작합니다.

### 일정 직접 만들기
```json
{
"workflowId": "8c2d7f1e-3a4b-4c5d-9e6f-7a8b9c0d1e2f",
"type": "schedule",
"config": {
"rules": [
{ "kind": "days", "every": 1, "hour": 9, "minute": 0 },
{ "kind": "weeks", "every": 2, "weekdays": [1, 3], "hour": 18, "minute": 30 }
],
"timezone": "Asia/Jerusalem"
}
}
```

`POST /v1/workflow-triggers`로 보내세요. 일정 트리거 노드가 저장하는 `config`도 같은 형태입니다.

| `config` 필드 | 의미 |
| --- | --- |
| `rules` | 규칙 하나 이상입니다. 맞는 규칙이 하나라도 있으면 워크플로가 실행됩니다. |
| `timezone` | 규칙을 해석할 시계로, `Asia/Jerusalem` 같은 시간대 이름입니다. 생략하면 UTC입니다. |
| `maxExecutions` | 이 횟수만큼 실행한 뒤 멈춥니다. 일정은 등록된 채로 남으므로, 계속하려면 숫자를 늘리거나 지우세요. |

| `kind` | 필드 | 실행 시점 |
| --- | --- | --- |
| `minutes` | `every` 1~59 | 매시간 0분, N분, 2N분, … |
| `hours` | `every` 1~23, `minute` | 매일 0시, N시, 2N시, …의 해당 분 |
| `days` | `every` 1~31, `hour`, `minute` | N번째 날마다 해당 시각 |
| `weeks` | `every` 1~52, `weekdays`(0은 일요일, 6은 토요일), `hour`, `minute` | N번째 주마다 해당 요일 |
| `months` | `every` 1~12, `dayOfMonth` 1~31, `hour`, `minute` | N번째 달마다 해당 일. 그달이 더 짧으면 마지막 날 |
| `cron` | 5개 필드로 된 표현식 `cron` | 표현식이 맞을 때마다 |

- **N일, N주, N개월 간격은 고정된 시작점부터 셉니다.** 주는 월요일에 시작하므로, 다시 저장해도 일정이 실행되는 날이 밀리지 않습니다.
- **`cron` 규칙에서는 모든 필드가 맞아야 합니다.** 날짜와 요일이 모두 맞아야 하며, 둘 중 하나만 맞아도 되는 일부 cron 도구와 다릅니다. `7`은 `0`과 마찬가지로 일요일입니다.
- **범위를 벗어난 값은 `400`으로 응답합니다.** 서버가 읽을 수 없는 시간대도 마찬가지입니다.
- **예전 형식도 계속 동작합니다.** 직접 만드는 트리거에는 `5m`, `1h`, `1d` 같은 `interval`이나 `cron` 문자열을 쓸 수 있습니다. 이 형식으로 저장한 일정 트리거 노드는 규칙으로 변환됩니다.

### 일정이 실행되는 방식
- 서버는 1분에 한 번 일정을 확인하고, 그 분에 맞는 규칙이 있는 워크플로를 실행합니다.
- 다시 시작하거나 시계가 바뀌어도 같은 분이 두 번 실행되지는 않습니다. 시계가 건너뛴 분은 그날 실행되지 않습니다.
- 워크플로의 이전 실행이 아직 진행 중이면 그 분은 건너뜁니다.
- 실행할 수 없는 노드는 **보류**됩니다. 예를 들어 표현식이 없는 `cron` 규칙이나 읽을 수 없는 시간대가 그렇습니다. 아무것도 추측하지 않으므로 노드를 고친 뒤 다시 저장하세요.

## 결과를 내 서버로 보내기
[Webhook 출력](https://nodaro.ai/docs/nodes/publish/webhook-output) 노드는 도달한 결과를 설정한 파라미터와 함께 `POST` 요청으로 내 URL에 보냅니다. 파라미터의 유형은 트리거와 같은 `text`, `imageUrl`, `videoUrl`, `audioUrl`입니다. 미디어 값은 Nodaro가 저장한 파일의 URL이며, 내 서버가 다운로드할 수 있습니다.

엔드포인트가 오류로 응답하면 노드가 실패하고, 오류가 실행 기록에 표시됩니다. `https` URL을 사용하세요.

### 요청에 키 함께 보내기
많은 엔드포인트는 헤더에 키가 있어야만 요청을 받습니다. 웹 앱의 **연동 › HTTP 자격 증명**에 헤더 이름과 키를 **자격 증명**으로 한 번 저장하고 노드에서 선택하세요. 키는 워크플로, 내보내기, 템플릿에 절대 들어가지 않습니다.

키를 저장할 때 누가 사용할 수 있는지 고릅니다.

| 선택 | 적용 대상 |
| --- | --- |
| **모든 주소**(일반 자격 증명) | 내가 직접 시작한 실행: 편집기에서 시작한 실행과 편집기에서 설정한 일정입니다. |
| **주소 한 곳만**(고정 자격 증명) | 모든 실행. 단, 그 주소로만, 또는 허용한 경우 그 아래 경로로만 보냅니다. |

API 토큰이나 OAuth 토큰으로 시작한 실행, Webhook 트리거, API로 만든 일정, 게시된 앱에서 시작한 실행은 나로서 실행되더라도 내가 직접 시작한 실행이 아닙니다. **이런 실행에는 자격 증명을 고정하세요.** 일반 자격 증명을 쓰면 노드가 키를 보내지 않고 명확한 메시지와 함께 실패합니다. 고정 자격 증명은 노드의 URL이 그 주소와 맞을 때만 보내지고, 다른 주소로의 리디렉션은 따라가지 않으며, 키는 평문 `http`로는 절대 보내지 않습니다.

자격 증명이 연결된 노드는 엔드포인트의 응답 본문을 보관하거나 표시하지 않고 상태 코드만 남깁니다. 자격 증명은 웹 앱에서만 관리할 수 있으며, 자격 증명 경로는 토큰에 `403 in_app_only`로 응답합니다. 앱을 게시하거나 다른 사람이 실행하도록 워크플로를 공유하면, 하위 워크플로 안의 것까지 포함해 함께 보내는 모든 자격 증명이 노드가 보내는 주소로 고정될 때까지 `409 credential_unbound`로 응답합니다.

## 엔드포인트 없이 실행 추적
실행 결과를 알 수 있는 다른 방법이 두 가지 있습니다.

- **폴링하세요.** `GET /v1/workflow-executions/:id`로 실행을 추적하거나, `GET /v1/jobs/:id/status`로 노드 하나의 작업을 추적하세요. [실행](https://nodaro.ai/docs/developers/api/executions)과 [작업](https://nodaro.ai/docs/developers/api/jobs)을 참고하세요.
- **워크플로가 알려 주게 하세요.** 결과를 내 서버로 보내는 Webhook 출력 노드로 워크플로를 마치세요.

## 자주 묻는 질문

### Webhook으로 Nodaro 워크플로를 시작하려면 어떻게 하나요?

Webhook 트리거 노드를 추가하고 파라미터를 정의한 뒤 워크플로를 저장하세요. 저장하면 POST /v1/webhooks/ 뒤에 토큰이 붙은 형태의 URL이 만들어집니다. 키가 파라미터 이름과 일치하는 JSON 본문을 보내면 되며, 다른 인증은 필요하지 않습니다.

### 작업이 끝나면 Nodaro가 내 서버를 호출하나요?

네. POST /v1/webhook-endpoints로 Webhook 엔드포인트를 등록하면 됩니다. 그러면 내 작업 하나가 끝날 때마다 Nodaro가 서명된 job.completed 또는 job.failed 이벤트를 내 https URL로 보내며, 약 43시간 동안 재시도합니다.

### Nodaro Webhook 이벤트는 어떻게 검증하나요?

엔드포인트의 시크릿으로 타임스탬프, 점, 원본 본문의 HMAC-SHA256을 계산해 Nodaro-Signature 헤더의 v1 값과 비교하세요. 타임스탬프가 최근인지도 확인하세요. SDK의 verifyWebhookSignature가 둘 다 처리합니다.

### Webhook URL을 공유해도 안전한가요?

비밀번호처럼 다루세요. URL에 든 토큰이 유일한 자격 증명이므로, 토큰을 가진 사람은 누구나 실행을 시작하고 내 크레딧이나 워크스페이스의 예산을 쓸 수 있습니다. 트리거마다 분당 10개의 요청을 받습니다.

### API로 워크플로 일정을 만들려면 어떻게 하나요?

데이터에 “active”를 true로 설정한 일정 트리거 노드를 저장하거나, POST /v1/workflow-triggers에 규칙과 시간대로 된 config를 보내 일정을 만드세요. Nodaro는 1분에 한 번 모든 일정을 확인합니다.

### API로 만든 일정 트리거가 실행되지 않는 이유는 무엇인가요?

일정 트리거는 데이터에 “active”가 true일 때만 실행됩니다. API, SDK, MCP로 작성한 노드에 이 필드가 없으면 일시 중지 상태로 등록됩니다. 필드를 설정하고 다시 저장하세요.
