parse-integration skill
비동기 OCR 및 스키마 기반 API를 사용하여 송장이나 영수증 같은 문서에서 구조화된 JSON 데이터를 추출합니다.
- GitHub 스타
- 8
- 파일
- 1
- 마지막 커밋
- 2026년 11월 10일
- 라이선스
- MIT
- 허용된 도구
- —
parse-integration skill은 무엇을 하나요?
parse-integration은(는) agent skill입니다. 비동기 OCR 및 스키마 기반 API를 사용하여 송장이나 영수증 같은 문서에서 구조화된 JSON 데이터를 추출합니다.
skill의 SKILL.md에서 생성된 요약입니다.
parse-integration skill 정보
이 SKILL은 개발자와 자동화된 에이전트가 송장, 영수증, 은행 거래 내역서와 같은 문서를 구조화된 JSON으로 변환할 수 있도록 지원합니다. 문서의 레이아웃이나 스캔 품질에 관계없이 커스텀 스키마를 사용하여 특정 필드를 식별하는 비동기 OCR 및 추출 파이프라인을 제공합니다. 사용자는 parse_* MCP 도구를 사용하여 즉시 추출을 수행하거나, Python 또는 Node.js와 같은 언어에서 HTTP API를 사용하여 맞춤형 프로덕션 통합 환경을 구축할 수 있습니다.
일반적인 워크플로우는 parse_extract를 통해 파일을 제출하고, parse_extraction_status로 상태를 폴링한 다음, 최종 데이터를 가져오거나 parse_export를 사용하여 CSV 또는 XLSX로 내보내는 과정을 포함합니다. 이 시스템은 사용자가 필드 이름, 유형 및 유효성 검사 규칙을 정의하여 정확성을 보장하는 스키마 기반 방식을 사용합니다. 프로세스가 비동기 방식으로 진행되므로, 이 SKILL은 처리 상태에서 완료 또는 실패 상태로의 전환을 관리하며 추출이 최종 상태에 도달했을 때만 데이터를 처리하도록 보장합니다.
agent가 수행할 수 있는 작업
- 1송장 데이터를 JSON으로 추출개발자가 PDF 송장을 제출하면 송장 번호 및 총액과 같은 특정 필드가 포함된 구조화된 JSON 객체를 받을 수 있습니다.
- 2웹훅을 통한 문서 처리 자동화엔지니어가 웹훅 URL을 설정하면 API를 지속적으로 폴링할 필요 없이 문서 추출이 완료되었을 때 알림을 받을 수 있습니다.
- 3맞춤형 문서 파싱 통합 구축제공된 API 엔드포인트와 인증 방식을 사용하여 개발자 고유의 소프트웨어에 문서 지능 기능을 통합할 수 있습니다.
skill의 SKILL.md를 기반으로 작성되었습니다. 모든 사용 사례는 해당 문서의 인용구와 대조 확인되었습니다.
parse-integration skill 설치 방법
각 명령은 skill 폴더를 클라이언트의 skills 디렉터리에 복사합니다. 클라이언트를 재시작하거나 새 세션을 시작하여 로드하세요.플러그인에 parse-integration skill 설치
/plugin marketplace add conversiontools/agent-skills /plugin install conversiontools@conversiontools-skills
Claude Code에 parse-integration skill 설치
~/.claude/skills/parse-integration/SKILL.md
$ npx degit conversiontools/agent-skills/skills/parse-integration ~/.claude/skills/parse-integrationClaude Code (project)에 parse-integration skill 설치
.claude/skills/parse-integration/SKILL.md
$ npx degit conversiontools/agent-skills/skills/parse-integration .claude/skills/parse-integrationCodex에 parse-integration skill 설치
~/.agents/skills/parse-integration/SKILL.md
$ npx degit conversiontools/agent-skills/skills/parse-integration ~/.agents/skills/parse-integrationGemini CLI에 parse-integration skill 설치
~/.gemini/skills/parse-integration/SKILL.md
$ npx degit conversiontools/agent-skills/skills/parse-integration ~/.gemini/skills/parse-integrationClaude.ai에 parse-integration skill 설치
npx degit conversiontools/agent-skills/skills/parse-integration parse-integration && zip -r parse-integration.zip parse-integration # claude.ai → Customize → Skills → + → Create skill → Upload a skill → parse-integration.zip
SKILL.md
GitHub에서 보기Parse: document data extraction
Parse turns documents into structured JSON. You describe the fields you want, send a PDF, scan, or photo, and get those fields back with their values. It handles the layout variation that breaks template-based parsers: the same schema works across suppliers, form revisions, and scan quality.
Two ways to use it, and this skill covers both.
- Run an extraction right now with the
parse_*MCP tools bundled in this plugin. - Build an integration in the user's own codebase against the HTTP API.
With a shell, the ctio CLI installed and a Parse key in CT_PARSE_TOKEN, ctio parse <file> --schema fields.json runs an extraction in one command and waits for the result (see the ctio section of the convert skill).
The one rule that decides whether an integration works
POST /v1/extract is asynchronous by default. It answers 202 with an extraction id and status: "processing" immediately, and the extracted data is not in that response. You then poll GET /v1/extractions/{id} until status is completed or failed.
Branch on status, never on timing. Every response carries status, so one code path handles all of them. Integrations that assume "if the call took a while, the data must be in there" break the first time a document is slow or fast in the wrong direction, and they break silently.
There are two shortcuts, and both still report status, so the same branch handles them:
wait=Nholds the request open for up to N seconds (max 120) and returns the result inline if it finishes in time. It only applies when the document is uploaded in the same call. If the window expires you get the usual202and id, and you poll.- A cache hit returns
status: "completed"withcached: trueinstantly. The same document with the same field definitions, already extracted on that account, is served from storage and costs no pages.
Using Parse from this agent (MCP tools)
| Tool | What it does |
|---|---|
parse_extract |
Submit a document. Returns an extraction id and status, not the data. |
parse_extraction_status |
Poll one extraction. Returns the data once status is completed. |
parse_list_schemas |
List saved field definitions on the account. |
parse_create_schema |
Create a reusable field definition. |
parse_export |
Turn a completed extraction into CSV or XLSX. |
parse_usage |
Pages used, page limit, remaining, reset date. |
These tools are in the server's parse toolset, which this plugin turns on. On claude.ai or in Cowork, connect the plugin's connector once first: Customize > Plugins > Conversion Tools > Connectors > Connect. Added the server by hand and the tools are missing? Re-add it as https://mcp.conversiontools.io/mcp?toolsets=all (or ?toolsets=convert,parse).
The loop is always: parse_extract, then parse_extraction_status every 2 to 3 seconds until terminal, then optionally parse_export. Check parse_list_schemas before inventing a new field list - reusing a schema keeps results consistent between runs and lets identical documents hit the cache.
Building an integration
Base URL: https://api-parse.conversiontools.io
Auth: an Authorization: Bearer <your API key> header on every request.
Step 1: get an API key
Create an account at parse.conversiontools.io, then go to Dashboard, API Keys (direct link) and create a key.
The key belongs to the user's application: it loads the key from its own secret manager at startup and passes it to the client (configure(api_key) in the reference implementations; <your API key> in the curl examples). Never commit it, never log it, never put it in client-side code - the key carries the account's full page allowance. Never ask the user to paste the key into the conversation.
Step 2: define a schema
A schema is a named list of fields. Save it once and reference it by schema_id, or pass an inline fields array for a genuine one-off.
Field rules, enforced server-side:
namemust be snake_case: lowercase letters, digits, underscores, starting with a letter.typeis one ofstring,number,date,boolean,array,object.- An
arrayneedsitems(string,number,date,boolean, orobject). Anobject, and anarrayofobject, needs a nestedfieldslist. - Optional per field:
description,required,examples,aliases, andvalidation(regexorenumfor strings,minormaxfor numbers). - Limits: 50 fields per level, 200 fields total, 4 levels of nesting.
description is the single biggest accuracy lever. "Invoice total including tax, bottom right, may be labelled Amount Due" beats total on its own. aliases covers the other labels a document might use for the same thing.
curl -X POST https://api-parse.conversiontools.io/v1/schemas \
-H "Authorization: Bearer <your API key>" \
-H "Content-Type: application/json" \
-d '{
"name": "Supplier invoice",
"description": "Fields we post into the ledger",
"fields": [
{ "name": "invoice_number", "type": "string", "required": true,
"description": "Supplier invoice reference, usually top right",
"aliases": ["Invoice No", "Rechnungsnummer"] },
{ "name": "invoice_date", "type": "date", "required": true },
{ "name": "currency", "type": "string",
"validation": { "enum": ["EUR", "USD", "GBP"] } },
{ "name": "total_amount", "type": "number",
"description": "Total including tax", "validation": { "min": 0 } },
{ "name": "line_items", "type": "array", "items": "object",
"fields": [
{ "name": "description", "type": "string" },
{ "name": "quantity", "type": "number" },
{ "name": "unit_price", "type": "number" },
{ "name": "amount", "type": "number" }
] }
]
}'
A bad field definition comes back as 400 with a details array naming the exact path, for example fields[0].name. Fix and resend. Nothing is billed for a rejected request.
Other schema endpoints: GET /v1/schemas, GET /v1/schemas/{id}, PUT /v1/schemas/{id} (full replace), DELETE /v1/schemas/{id}.
Step 3: submit a document
One call, multipart/form-data:
curl -X POST https://api-parse.conversiontools.io/v1/extract \
-H "Authorization: Bearer <your API key>" \
-F "file=@invoice.pdf" \
-F "schema_id=SCHEMA_ID"
{ "success": true, "id": "65aa28ae...", "status": "processing" }
Form fields on this call: file (required), schema_id or fields (a JSON string), wait, webhook_url, no_cache, output_format.
Or split it into upload and extract, which is the better shape when you extract the same file more than once, or want the upload and the extraction on different code paths:
# 1. upload -> file_id
curl -X POST https://api-parse.conversiontools.io/v1/upload \
-H "Authorization: Bearer <your API key>" \
-F "file=@invoice.pdf"
# 2. extract by file_id (JSON body)
curl -X POST https://api-parse.conversiontools.io/v1/extract \
-H "Authorization: Bearer <your API key>" \
-H "Content-Type: application/json" \
-d '{ "file_id": "FILE_ID", "schema_id": "SCHEMA_ID" }'
JSON body fields: file_id (required), schema_id or fields (an array), webhook_url, no_cache, output_format. Note that wait is not available on this path - submit, then poll.
Send no_cache only when you actually want to bypass the cache. Any value present is read as "bypass", so a literal false still disables caching. Leave the field out instead.
Step 4: poll until terminal, with bounded retries
curl https://api-parse.conversiontools.io/v1/extractions/EXTRACTION_ID \
-H "Authorization: Bearer <your API key>"
{
"success": true,
"id": "65aa28ae...",
"filename": "invoice.pdf",
"status": "completed",
"data": { "invoice_number": "INV-2026-0042", "total_amount": 1284.5, "line_items": [ ... ] },
"pages_used": 2,
"validation": { "status": "passed", "warnings": [], "coercions": [] },
"error": null,
"created_at": "2026-07-26T10:00:00.000Z",
"completed_at": "2026-07-26T10:00:37.000Z"
}
Poll every 2 to 3 seconds with a small backoff, and stop on a wall-clock deadline rather than a retry count - a long document legitimately takes a minute or more, and a fixed count silently means different things at different intervals. Treat hitting the deadline as its own outcome, not as a failure of the extraction: the extraction keeps running server-side and the id stays valid, so you can requeue the poll later.
validation reports how well the result matched the requested fields: passed, partial (usable, but read warnings), failed (required fields missing or wrong-typed, treat the data as unreliable), or unvalidated (no field definitions were supplied). Surface partial and failed to a human instead of writing them straight into a ledger.
Step 5: handle failed
{ "status": "failed", "error": "...", "pages_used": 1 }
A failed extraction is not billed - the reserved page is refunded. Do not blind-retry the same bytes with the same schema: it will fail the same way. Log the id and the error, and route it to a human or to a fallback. Retrying is only sensible after the input or the field definitions change.
Re-sending the same document while an extraction is still running returns the same id instead of starting a duplicate, so a retry after a network blip is safe and costs nothing extra.
Step 6: webhooks instead of polling
Pass webhook_url at submit time and Parse posts to it when the extraction reaches a terminal state:
{
"event": "extraction.completed",
"id": "65aa28ae...",
"status": "completed",
"pages_used": 2,
"error": null,
"created_at": "...",
"completed_at": "..."
}
The payload deliberately carries the id and status only, never the extracted data. Your handler treats it as a trigger and fetches the result over the authenticated API. That way a misconfigured or hijacked endpoint cannot leak anyone's documents.
Requirements and behaviour:
- The URL must be publicly reachable over http or https. Private and internal addresses are rejected at submit time with
400. - Delivery is fire and forget with a short timeout and a few bounded retries. A webhook can be missed, so keep a slow reconciliation poll for anything you have not heard about.
- There is no signature on the payload. Use an unguessable path, treat the URL itself as a secret, and re-fetch through the API before acting on anything. Never trust the body as the source of truth.
Step 7: export to a spreadsheet
Exports run the extracted JSON through a normal conversion. They are free, repeatable, and never re-run the extraction.
# start it (idempotent per format - a running export is reused)
curl -X POST https://api-parse.conversiontools.io/v1/extractions/EXTRACTION_ID/export \
-H "Authorization: Bearer <your API key>" \
-H "Content-Type: application/json" \
-d '{ "format": "xlsx" }'
Then poll GET /v1/extractions/EXTRACTION_ID/export?format=xlsx with the same Authorization header and save the body once it is the file; the Python and Node clients below do exactly that. The GET speaks two languages: application/json with {"status": "processing", "progress": N} while it converts, and the file's own bytes once it is ready. Check the response Content-Type, do not assume you got a file. Nested arrays are flattened into rows with the parent values repeated, which is the layout spreadsheet users expect.
You can also pass output_format: "csv" | "xlsx" at submit time to start the export automatically as soon as the extraction completes.
Other endpoints
GET /v1/usage-plan,pages_used,pages_limit,pages_remaining,reset_date. One page is billed per document page. Failed extractions are refunded, cache hits cost nothing.DELETE /v1/extractions/{id}- deletes the stored result and its exports. Results are kept until you delete them, so wire this into the user's data-retention policy.
Errors worth handling explicitly
| Status | Meaning | What to do |
|---|---|---|
400 |
Bad request. details names the exact field path. |
Fix the schema or parameters and resend. Nothing was billed. |
401 |
Missing, malformed, or revoked key. | Check the Authorization header and the key. Do not retry. |
404 |
No such extraction or schema on this account. Ids are per-account. | Check the id. Do not retry. |
409 |
Export requested before the extraction completed. | Poll until completed, then export. |
410 |
The stored result is gone. | Re-submit the document with no_cache: true. |
429 |
Monthly page allowance used up. | Stop. Retrying fails the same way. Check GET /v1/usage, raise the plan at Dashboard, Billing. |
5xx |
Server-side. | Retry a couple of times with backoff, then give up and report. |
Rule of thumb: retry 5xx and network errors, never 4xx.
Reference implementation: Python
Python 3.9+ with the requests package.
"""Minimal, production-shaped Parse client: submit, poll until terminal, export."""
import json
import time
from pathlib import Path
from typing import Any, Optional
import requests
BASE_URL = "https://api-parse.conversiontools.io/v1"
HEADERS: dict[str, str] = {}
def configure(api_key: str) -> None:
"""Call once at startup. The key comes from your app's secret manager, never from this file."""
HEADERS["Authorization"] = f"Bearer {api_key}"
class ParseError(RuntimeError):
"""An API call was rejected."""
def __init__(self, status: int, message: str, details: Any = None):
super().__init__(f"HTTP {status}: {message}")
self.status = status
self.details = details
class ExtractionFailed(RuntimeError):
"""The document was processed but produced no usable result."""
class ExtractionPending(RuntimeError):
"""Still processing when our deadline ran out. The id stays valid."""
def _unwrap(response: requests.Response) -> dict:
if response.ok:
return response.json()
try:
body = response.json()
except ValueError:
body = {}
raise ParseError(
response.status_code,
body.get("message") or body.get("error") or response.reason,
body.get("details"),
)
def create_schema(name: str, fields: list[dict], description: str = "") -> str:
payload = {"name": name, "fields": fields, "description": description}
body = _unwrap(requests.post(f"{BASE_URL}/schemas", headers=HEADERS, json=payload, timeout=30))
return body["schema"]["id"]
def submit(
path: str | Path,
*,
schema_id: Optional[str] = None,
fields: Optional[list[dict]] = None,
webhook_url: Optional[str] = None,
no_cache: bool = False,
) -> dict:
"""Submit a document. Returns the envelope with `id` and `status`.
This does NOT return the extracted data - poll wait_for_result() next.
"""
form: dict[str, str] = {}
if schema_id:
form["schema_id"] = schema_id
elif fields:
form["fields"] = json.dumps(fields)
if webhook_url:
form["webhook_url"] = webhook_url
if no_cache:
# Send the field only when bypassing the cache: any value present counts.
form["no_cache"] = "true"
path = Path(path)
with path.open("rb") as handle:
response = requests.post(
f"{BASE_URL}/extract",
headers=HEADERS,
files={"file": (path.name, handle)},
data=form,
timeout=120,
)
return _unwrap(response)
def get_extraction(extraction_id: str) -> dict:
return _unwrap(requests.get(f"{BASE_URL}/extractions/{extraction_id}", headers=HEADERS, timeout=30))
def wait_for_result(extraction_id: str, *, timeout_s: float = 600.0) -> dict:
"""Poll until terminal. Branches on `status`, never on elapsed time."""
deadline = time.monotonic() + timeout_s
interval = 2.0
while time.monotonic() < deadline:
body = get_extraction(extraction_id)
status = body.get("status")
if status == "completed":
return body
if status == "failed":
raise ExtractionFailed(body.get("error") or "extraction failed")
# "pending" / "processing" -> keep waiting
time.sleep(interval)
interval = min(interval * 1.5, 10.0) # gentle backoff, capped
raise ExtractionPending(
f"{extraction_id} still processing after {timeout_s:.0f}s. "
"It is still running server-side; poll this id again later."
)
def export(extraction_id: str, fmt: str, out_path: str | Path, *, timeout_s: float = 300.0) -> Path:
"""Export a COMPLETED extraction to csv or xlsx and save it."""
_unwrap(requests.post(
f"{BASE_URL}/extractions/{extraction_id}/export",
headers=HEADERS, json={"format": fmt}, timeout=30,
))
deadline = time.monotonic() + timeout_s
while time.monotonic() < deadline:
response = requests.get(
f"{BASE_URL}/extractions/{extraction_id}/export",
headers=HEADERS, params={"format": fmt}, timeout=60,
)
if not response.ok:
_unwrap(response)
# JSON means "still converting" or "failed"; anything else is the file.
if response.headers.get("content-type", "").startswith("application/json"):
state = response.json()
if state.get("status") == "failed":
raise ExtractionFailed(state.get("error") or "export failed")
time.sleep(2.0)
continue
out_path = Path(out_path)
out_path.write_bytes(response.content)
return out_path
raise ExtractionPending(f"export of {extraction_id} still converting after {timeout_s:.0f}s")
def main(api_key: str) -> None:
configure(api_key)
submitted = submit("invoice.pdf", schema_id="<your schema id>")
print("submitted:", submitted["id"], submitted["status"])
try:
# A cache hit already comes back completed - the same branch handles it.
result = submitted if submitted["status"] == "completed" else wait_for_result(submitted["id"])
except ExtractionFailed as exc:
raise SystemExit(f"extraction failed, not retrying: {exc}")
print("pages billed:", result.get("pages_used"), "cached:", result.get("cached", False))
print(json.dumps(result["data"], indent=2, ensure_ascii=False))
if (result.get("validation") or {}).get("status") in {"partial", "failed"}:
print("needs review:", result["validation"].get("warnings"))
export(result["id"], "xlsx", "invoice.xlsx")
if __name__ == "__main__":
from getpass import getpass
main(getpass("Parse API key: ")) # for a one-off run; a service injects it from its secret manager
이 스킬의 파일
1parse-integration skill 질문
Claude Code에 parse-integration skill을 어떻게 설치하나요?
npx degit conversiontools/agent-skills/skills/parse-integration ~/.claude/skills/parse-integration를 실행합니다. Claude Code는 ~/.claude/skills(모든 프로젝트) 또는 .claude/skills(한 프로젝트)에서 skills를 로드하며, 설명이 작업과 일치할 때 skill을 사용합니다.
parse-integration skill을 사용할 수 있는 클라이언트는 무엇인가요?
Agent Skills 형식을 읽는 모든 클라이언트(Claude Code, Codex, Gemini CLI, claude.ai(ZIP으로 업로드))가 사용할 수 있습니다. skill 내 스크립트는 클라이언트가 코드 실행을 허용하는 경우에만 실행됩니다.
추출 결과를 확인하기 위해 폴링 대신 웹훅을 사용할 수 있나요?
네, 제출 시점에 webhook_url을 제공할 수 있습니다. 추출이 최종 상태에 도달하면 Parse가 해당 URL로 포스트를 보냅니다. 다만 웹훅은 전송 후 확인하지 않는 방식이므로, 데이터 정합성을 위해 재조회(reconciliation) 폴링을 함께 구현하는 것이 좋습니다.
추출에 실패하면 어떻게 되나요?
실패한 추출에 대해서는 비용이 청구되지 않으며 예약된 페이지 비용은 환불됩니다. 오류를 로그에 기록하고, 동일한 문서를 동일한 스키마로 무작정 재시도하기보다는 담당자에게 전달하여 검토하도록 구성해야 합니다.