space ocr
文章文档
指南

发票 OCR API/送货单 OCR 转 CSV ── 发票数据提取 API 实战指南

把发票、送货单从手工录入和 Excel 乱码中解放出来的开发者指南。向 POST /ocr/fields 上传图片,即可返回交易方、日期、合计、明细等结构化数据,每个值都附带原图坐标(bbox)和 match_ratio。含 curl/Python 代码、CSV 导出、Webhook 与价格。

9 分钟阅读· 2026-06-25

你是不是到现在还在手动把发票、送货单一个个敲进 Excel?日期、交易方、税前、税后,还有明细的每一行 ── 一到月底,就对着堆成山的纸张较劲,把数字一格一格地誊抄。中途某一位数字错了位,合计对不上,又得从头核对一遍。这些时间,真希望能省下来。

想复制扫描出来的 PDF,结果文字根本选不中。拿去做 OCR,明细又全挤进了一个单元格,换行和列全没了。用 Excel 打开 CSV,又是一片乱码,品名压根读不出来。明明只想导进财务软件,却总是卡在这最后一步 ── 这就是天天和单据打交道的人都熟悉的「老大难」。

本文是一份开发者指南,教你把这些活儿换成一个 API。向 POST /ocr/fields 上传发票、送货单的图片,交易方、日期、合计等字段,以及明细的每一行,都会以带类型的结构化数据返回。更关键的是,返回的每一个值都附带它在原图中被识别出来的坐标(bbox),所以你不必盲目相信提取结果,而是可以拿原件逐一比对校验。下面配合 curl 和 Python 代码,从最快上手到生产环境,完整走一遍。

先上手试试 ── 无需上传,10 秒体验

写代码之前,先看看实际输出。下面是解析一张真实小票的结果。把光标移到某个字段上,就会高亮出这个值是从图片的哪个位置读出来的,同时还能看到每个字段的匹配率(match_ratio)。发票、送货单的表现完全一样 ── 提取出的每一个值,都对应到它被读取的那块像素。

Source receipts with extracted-field bounding boxes
Verified fields
KINSHO · 合計 2,045
ライフ · 合計 4,286

Every value carries a verified on-page location — bbox + 4-point vertices + match_ratio — on a 0–1000 normalized grid (0,0 top-left → 1000,1000 bottom-right), the same shape the live API returns. Hover a field to trace it back to the pixels it came from.

Demo提取出的每个值都会随附 <b>bbox(坐标)</b>、支持旋转的 vertices 以及<b>匹配率</b>一起返回 ── 是可以绘制、可以引用、带「出处」的数据。
提取出的每个值都会随附 bbox(坐标)、支持旋转的 vertices 以及匹配率一起返回 ── 是可以绘制、可以引用、带「出处」的数据。

「原图 → 提取表格 → 高亮对应位置 → 导出 CSV」的流程

space ocr 的用法,归根结底就是 4 步。(1) 上传收据、发票、送货单的图片 → (2) 按固定列结构提取成一张表格,一图=一行 → (3) 点击某个值,原图的对应位置就会点亮,方便和原件核对 → (4) 直接导出 CSV 导入财务软件。先从上传一张、看着字段被自动填满开始。

Demo拖入一张发票,带类型的字段就会自动填好 ── 和 <b>API</b> 返回的是同一份数据,只不过呈现在 UI 上。
拖入一张发票,带类型的字段就会自动填好 ── 和 API 返回的是同一份数据,只不过呈现在 UI 上。

鉴权与基础 URL

公开 API 只有一个基础地址 https://api.space-ocr.com ── 没有 /v1 这样的路径版本号。每个请求都用以 spocr_ 开头的密钥,通过 HTTP Bearer Token 进行鉴权。

Authorization: Bearer spocr_xxxxxxxxxxxxxxxx

如果请求头缺失或不合法会返回 401,密钥未注册则返回 403。所有响应都带有 X-Request-Id(格式为 req_xxx)请求头,建议记录到日志里,方便日后联系支持时排查。如果想自动生成客户端,可在 GET /openapi.json 获取公开的 OpenAPI 3.1 规范。

最快路径 ── 内置的发票、送货单模板

最快的方式,是在 templateId 里指定内置模板。发票用 templateId: "invoice",送货单用 templateId: "delivery"。引擎本身就知道「发票应该有哪些字段」,所以你不必再一个个去定义。图片既可以传 URL,也可以传纯 base64(imageType 会根据是否带 http(s):// 前缀自动判断)。

POST /ocr/fields ── 用送货单模板调用
1
2
3
4
5
6
7
8
curl -X POST https://api.space-ocr.com/ocr/fields \
  -H "Authorization: Bearer spocr_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "image": "https://example.com/docs/delivery-0831.jpg",
    "imageType": "url",
    "templateId": "delivery"
  }'
Why it matters

参数的正式写法是驼峰式(camelCase)。 请使用 imageType / templateId / autoFields。旧的下划线写法(image_type / template_id / auto_fields)虽然也能用,但已不推荐。新代码请优先使用驼峰式。另外,发票用 templateId: "invoice",送货单用 templateId: "delivery"

响应的结构 ── 每个值都带「出处」

成功时会返回 { status: "success", data: { ... } }。提取出的每个值都带有出处信息,所有字段的坐标都汇总在 field_bboxes 映射里。

  • bbox ── { xmin, ymin, xmax, ymax } 形式的轴对齐矩形。坐标是归一化到 0–1000网格上的整数(0,0 为左上角,1000,1000 为右下角),与图片的像素尺寸无关。换算到像素:pixel_x = bbox_x / 1000 × image_width
  • vertices ── 按左上 → 右上 → 右下 → 左下顺序排列的 4 个点 {x, y}。这是顺着单据倾斜方向的**支持旋转(oriented)**矩形,所以哪怕是斜着拍的手机照片,也能贴合地框住。
  • match_ratio ── 该值的字符中,实际在页面上找到的比例(0–1)。0.85 及以上会被视为确信匹配,1.0 表示所有字符都在页面上找到了。
  • bbox_source ── 标记坐标是如何推导出来的。vision_symbol_match(字符匹配达到 0.85 以上而落点的常规路径 ── 随附真实的 match_ratio)、token_id / token_id_hybrid(用 LLM 返回的词元提示去检索 Vision 的词 token 的路径)、low_confidence(字符匹配不足 0.85 ── 需要确认)、shared_value(来自合并单元格的传播)。
POST /ocr/fields → 响应(节选)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
  "status": "success",
  "data": {
    "total": "2,045",
    "field_bboxes": {
      "total": {
        "bbox": { "xmin": 595, "ymin": 974, "xmax": 781, "ymax": 1000 },
        "vertices": [
          { "x": 594, "y": 975 }, { "x": 781, "y": 972 },
          { "x": 781, "y": 998 }, { "x": 595, "y": 1000 }
        ],
        "match_ratio": 0.93,
        "bbox_source": "vision_symbol_match"
      }
    }
  }
}
✓ Verified

坐标并不会盲目相信 AI 的一面之词。 语言模型返回的只是每个值的文本,以及它所用词元的提示(wid),并不返回坐标本身。引擎会先把这段文本,与 Vision OCR 在页面上实际检测到的符号逐字符比对 ── 所以矩形会落到这些字符真正出现的像素上,每个值也都会附带一个表示「匹配了多少」的 match_ratio。当 LLM 返回了词元提示时,引擎有时会用该 token 的坐标去覆盖部分字段,但这种提示可能带有噪声(在重复行里会和相邻行搞混,即所谓 stochastic 的抖动),所以不会盲信,而是先用列、行的一致性做校验和修正后再采用。要点不是「AI 不会出错」,而是每一个值都会回到页面侧重新比对,并留下一个匹配程度的分数。详情请参阅用边界框让 OCR 可审计的机制

模板不够用时 ── 自定义字段

实务中的发票,常常有通用模板叫不上名字的字段 ── 比如采购单号、付款条件代码、项目标签等。这时可以用字段规格(FieldSpec)数组 fields 来替代 templateId(也可以两者并用)。每个 FieldSpec 形如 { name, type, description?, children? }。如果 fieldstemplateId 同时发送,则以 fields 为准。

description 就是引导模型的地方 ── 你可以用一段中文(或日文)指令写清楚「拾取什么、怎么拾取」。而 type: "array"children 的组合,正是抽取重复明细行的方法。只要定义一份子结构,就能返回任意多行。

自定义 FieldSpec ── 嵌套提取明细(送货单转 CSV)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import requests, base64, csv

with open("delivery.jpg", "rb") as f:
    b64 = base64.b64encode(f.read()).decode()

resp = requests.post(
    "https://api.space-ocr.com/ocr/fields",
    headers={"Authorization": "Bearer spocr_xxxxxxxxxxxxxxxx"},
    json={
        "image": b64,
        "imageType": "base64",
        "fields": [
            {"name": "vendor", "type": "string",
             "description": "发货方(交易方)的公司名称"},
            {"name": "delivery_no", "type": "string",
             "description": "送货单号。保持原文"},
            {"name": "delivery_date", "type": "string",
             "description": "送货日期。和历、公历均保持原文"},
            {"name": "total", "type": "string",
             "description": "合计金额。保留千位分隔符"},
            {"name": "items", "type": "array",
             "description": "每条明细对应一个元素",
             "children": [
                 {"name": "name", "type": "string", "description": "品名"},
                 {"name": "qty", "type": "number", "description": "数量"},
                 {"name": "unit_price", "type": "number", "description": "单价"},
             ]},
        ],
    },
    timeout=60,
)

data = resp.json()["data"]

# 把明细写入 CSV。为兼容 Excel 与 CJK,用带 BOM 的 UTF-8 导出
with open("delivery.csv", "w", encoding="utf-8-sig", newline="") as out:
    w = csv.writer(out)
    w.writerow(["品名", "数量", "单价"])
    for row in data.get("items", []):
        w.writerow([row["name"], row["qty"], row["unit_price"]])
Why it matters

值会按原文(verbatim)保留。 合计 7,855 会作为字符串 "7,855" 返回,千位分隔符、小数点、全角字符都原样保留。只有在 description 里明确要求时才会做归一化。UI 上看到的 ¥ 只是装饰,不属于值的一部分。应对 CSV 乱码的诀窍是:要用 Excel 打开的 CSV,请用 UTF-8 BOMutf-8-sig)导出 ── 上面的代码就是这么做的。另外,防止明细「被挤进一个单元格」的关键就是 type: "array" + children,它会把一条明细展开成一行。

点一下文字,跳到原图对应位置

数据沉淀到表格之后,点击某个值,原图的对应位置就会点亮。这是批量抽查时最快的办法 ── 不必把整张单据扫一遍,视线直接落到那个位置。你也可以只优先复核 match_ratio 较低的字段,按需运作。

Demo跨已提取的发票做全局检索,命中的单元格 ── 以及它在<b>原图中的对应位置</b> ── 一键直达。
跨已提取的发票做全局检索,命中的单元格 ── 以及它在原图中的对应位置 ── 一键直达。

异步批量处理 ── 批量上传、任务、Webhook

POST /ocr/fields 是同步的,最适合放进请求/响应循环里做单张处理。如果要整批处理一整个发票、送货单文件夹,就对某张表格用 POST /upload(multipart 里重复 files)提交。默认会立即返回一个任务数组。

{ "path": "...", "jobs": [ { "uniqueKey": "...", "jobId": "...", "status": "pending" } ] }

获取结果有两种方式:轮询 GET /jobs/{jobId},或者注册 Webhook。Webhook 每个空间一个 URL,所有事件都用 X-Spaceocr-Signature 请求头做 HMAC-SHA256 签名。值得关注的事件有 upload.receiveditem.createdocr.completed(提取结果在 data.result 中)、ocr.failed。在信任载荷之前,请务必先校验签名。

幂等性、请求追踪与限流

为了让生产流水线能安全重试,有几个请求头可用。

请求头作用
Idempotency-Key/upload/create 上,同一 key 的重发会在 24 小时内回放缓存响应(X-Idempotent-Replay: true)── 既能安全重试又不会重复扣费。
X-Request-Id所有响应都会带(req_xxx)。记录到日志便于支持排查。

限流为每个密钥 60 请求/分钟每个 uid 600 请求/分钟(固定 60 秒窗口)。超出时会返回 429error.code: "rate_limited"。等待秒数放在 JSON 响应体的 details.retryAfterSec 里 ── 不是 Retry-After HTTP 请求头。请依据响应体里的这个值来做退避。

429 响应体
1
2
3
4
5
6
7
8
{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded",
    "requestId": "req_8fa2c1"
  },
  "details": { "retryAfterSec": 12 }
}

从提取,到可查询的表格

把发票提取进表格之后,要回头读取数据时,不必再跑一次 OCR。GET /view 会在已沉淀的行上执行服务端查询 ── wheresortselectlimitoffset ── 既不重跑 OCR,也不计费。坐标默认会一起返回,想轻量化时再加 boxes=0。比如用 where=total>=40000 只看高额发票,用 sort=-invoice_date 按从新到旧排序。从这里导出 CSV(带 UTF-8 BOM,所以 Excel 和 CJK 都能正常打开),就能用于导入财务软件 ── 详情请参阅把扫描单据转成 CSV把收据转成 CSV。所有端点的规范都汇总在 API 文档里。

Note

PDF 需先把页面转成图片再发送。 OCR 引擎直接解析的是栅格图像(JPEG、PNG、GIF、BMP、TIFF、WebP)。如果直接调用 API,请先把 PDF 的每一页渲染成 PNG 等图片再发送(如果是拖入 Web 应用,应用会自动把页面图像化,所以可以直接丢 PDF 进去)。与 freee、マネーフォワード(Money Forward)、弥生(Yayoi)、kintone 的对接并非官方 API 集成,而是默认通过导入导出的 CSV 来完成。另外,是否符合发票留存制度(インボイス制度)或电子账簿保存法(電子帳簿保存法),请结合各家的运营与要求自行确认(本服务并不保证满足法定要求)。

价格

POST /ocr/fields每次调用 ¥10POST /upload 是 ¥10 × N 张。失败不计费 ── OCR 没返回结果就会退款,502 引擎错误或 ocr.failed 事件都会自动退款。只读端点(GET /space/view/amount/health)免费。免费额度无需信用卡,每月 100 张;Pro 为 $39/月;Business 请联系咨询(按需报价)。

用 API 提取发票、送货单的步骤

  1. 准备 API 密钥
    登录后签发以 spocr_ 开头的 API 密钥,在每个请求里加上 Authorization: Bearer spocr_...。基础 URL 为 https://api.space-ocr.com。
  2. 准备图片(PDF 先把页面图像化)
    把发票、送货单准备成 JPEG/PNG 等栅格图像。如果直接调用 API,PDF 要先把每一页渲染成 PNG 再发送(拖入 Web 应用时应用会自动图像化)。图片可用 URL 或纯 base64 传入,并用 imageType 指定 url / base64。
  3. 调用 POST /ocr/fields
    发票指定 templateId: "invoice",送货单指定 templateId: "delivery"。模板覆盖不到的字段用 fields[](FieldSpec {name,type,description,children})定义,明细用 type:"array" + children 逐行展开。
  4. 校验响应
    检查返回的每个值的 bbox、vertices、match_ratio、bbox_source。对于 match_ratio 不足 0.85(low_confidence)的字段,拿原件比对确认。
  5. 导出 CSV 导入财务软件
    把提取结果导出为带 UTF-8 BOM 的 CSV(明细作为数组行展开),交给 freee、マネーフォワード(Money Forward)、弥生(Yayoi)等的 CSV 导入。数据沉淀后,可用 GET /view 在不重跑 OCR、不计费的情况下查询。
发票、送货单 OCR API 支持中文吗?
支持。语言完全自动识别,无需指定提示。中文、日文、英文、韩文都由同一个引擎处理,并会对全角/半角、连字符的各种变体、括号、CJK 空白、竖排汉字、多种文字混排进行归一化。和历日期和品名等,只要不在 description 里明确要求,都会按原文(verbatim)返回。
支持 PDF 格式的发票、送货单吗?
支持。不过 OCR 引擎直接解析的是栅格图像(JPEG、PNG、GIF、BMP、TIFF、WebP)。如果直接调用 API,请先把 PDF 的每一页渲染成 PNG 等图片再发送。如果是把 PDF 拖入 Web 应用,应用会自动把页面图像化,所以可以直接丢 PDF 进去做 OCR。
提取出的数据能导入 freee、マネーフォワード(Money Forward)、弥生(Yayoi)吗?
可以通过 CSV 导入。表格能导出为 CSV(为兼容 Excel 与 CJK,带 UTF-8 BOM),明细会作为数组行展开,因此可以交给各家财务软件的 CSV 导入功能。这并非官方 API 集成,而是默认通过导出的 CSV 来导入运作。
提取精度如何保证?结果可信吗?
每个值都会附带它在原图中被读取位置的 bbox(0–1000 归一化坐标)、vertices,以及 match_ratio。match_ratio 是该值的字符中实际在页面上找到的比例,0.85 及以上为确信匹配,1.0 表示所有字符都匹配。坐标不是 AI 凭空生成的,而是把提取出的文本与 Vision OCR 的真实符号逐字符比对后推导出来的。由于 LLM 返回的词元提示可能带有噪声,会先用列、行的一致性做校验后再采用。因此你可以做这样的审计运作:只优先目视复核分数较低的字段。
个人信息和数据如何处理?失败时如何计费?
失败不计费。OCR 没返回结果就会退款,502 引擎错误或 ocr.failed 事件都会自动退款。Webhook 每个空间一个 URL,所有事件都用 X-Spaceocr-Signature 请求头做 HMAC-SHA256 签名,接收方可以先校验签名再处理。用幂等键(Idempotency-Key)还能防止重试时重复扣费。

一次调用,提取你的第一张发票

免费额度 ── 每月 100 张,无需信用卡。每一个值都附带它在原图中被读取位置的坐标一起返回。

相关文章