SignTulip

SignTulip API

REST API와 웹훅으로 전자서명을 서비스에 연동하세요.

인증

설정 → API·웹훅에서 발급한 키를 Authorization 헤더에 Bearer 토큰으로 보내세요.

curl https://signtulip.com/api/v1/templates \
  -H "Authorization: Bearer st_live_..."

요청 한도: API 키당 분당 120회

엔드포인트

GET/api/v1/templatesList templates (yours and your team's).
POST/api/v1/documentsCreate a document from a template and send it.
GET/api/v1/documentsList documents. Query: status, limit (≤100), offset.
GET/api/v1/documents/:idDocument status, recipients and signing links.
GET/api/v1/documents/:id/downloadRedirect to the signed PDF (?type=original for the original).
POST/api/v1/documents/:id/voidVoid a pending document. Body: { reason }.
curl -X POST https://signtulip.com/api/v1/documents \
  -H "Authorization: Bearer st_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "template_id": "5b0c…",
    "title": "NDA — Acme Inc.",
    "locale": "en",
    "expires_in_days": 14,
    "recipients": [
      { "role": "Signer 1", "name": "Jane Doe", "email": "jane@acme.com" }
    ]
  }'
// 201 Created
{
  "data": {
    "id": "8f3e…",
    "title": "NDA — Acme Inc.",
    "status": "pending",
    "recipients": [
      { "name": "Jane Doe", "email": "jane@acme.com", "status": "sent",
        "signing_url": "https://signtulip.com/sign/…" }
    ]
  }
}

웹훅

이벤트가 발생하면 등록한 URL로 JSON을 POST합니다. SignTulip-Signature 헤더로 요청을 검증하세요.

이벤트 종류

document.sentrecipient.viewedrecipient.signedrecipient.declineddocument.completeddocument.voideddocument.expired
{
  "id": "evt_…",
  "type": "document.completed",
  "created_at": "2026-09-27T09:00:00.000Z",
  "data": { "document": { "id": "8f3e…", "status": "completed", "recipients": [ … ] } }
}

서명 검증

// SignTulip-Signature: t=1695804000,v1=5f2b…
import crypto from "node:crypto";

function verify(rawBody, header, secret) {
  const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300;
  return fresh && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}

오류 응답

오류는 { "error": { "code", "message" } } 형식이며 HTTP 상태 코드와 함께 반환됩니다.

{ "error": { "code": "template_not_found", "message": "Template not found." } }