space ocr
指南文章价格文档

用于提取发票数据的 API

space-ocr 发票数据提取 API 开发者指南:用 curl 与 Python 调用 POST /ocr/fields、声明 fields 结构或使用 autoFields,以及每个值的来源坐标(box·quad)与 review 契约。

从一张发票里抽取结构化数据——供应商、发票号、日期、行项目金额、税额——是最常见的文档自动化需求之一,也是手写最折腾的一类。靠正则去匹配 OCR 文本,供应商一改版式就全盘失效;模板匹配类工具又要你给每个供应商手动框选区域。你真正想要的,是一个能读懂任意版式、返回干净的强类型字段,并且——这点至关重要——能告诉你每个值在页面上的来源位置、从而让结果值得信任的发票数据提取 API。

最后这点才是关键。一个只丢回 total: 2,045、却不带任何溯源信息的发票提取接口,放进应付账款(accounts payable)流程里就是个隐患。本指南会带你走一遍 space-ocr 的 POST /ocr/fields 接口:一次同步调用,传入一张发票图片,套用你声明的字段结构(或交给 autoFields 自动提议),即可返回每个值及其来源坐标与明确的校验判定。

先看输出,再写代码

下面是一张真实解析后的收据。把鼠标悬停在任意字段上,图片上对应的框就会高亮——那个框正是该值被读取出来的位置,而每个值都带着自己的校验判定与佐证。发票的处理方式完全一致:你提取的每个字段,都会落回它原本来源的像素上。

Invoice with extracted-field bounding boxes
Verified fields
Invoice

Each value with a box carries a verified on-page location — in data.cells[path], that is box + 4-point quad + evidence.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.

鉴权与基础 URL

公开 API 只有一个统一基础地址——https://api.space-ocr.com——没有 /v1 之类的路径版本号。每个请求都用 HTTP Bearer token 鉴权,密钥以 spocr_ 为前缀:

1
Authorization: Bearer spocr_xxxxxxxxxxxxxxxx

密钥缺失或无效会返回 401(error.code: "invalid_api_key")。403 的含义完全不同:密钥本身有效,但访问的资源不在该密钥的范围内(例如另一把密钥创建的 job)。每个响应都带有一个 X-Request-Id 头(格式为 req_xxx),建议记录下来用于排查支持问题。完整规范以 OpenAPI 3.1 的形式发布在 GET /openapi.json,如果你想直接生成客户端代码可以用它。

最简单的调用:显式声明发票字段结构

最快的方式是把你要的字段直接写出来。fields 接收一个由 FieldSpec 对象组成的数组,响应会严格按照这份声明的形状返回——不用挑模板,也不用画框。imageType 是必填参数,用来说明 image 的承载方式:"url" 或 "base64"。

声明标量 type 不只是记录意图。把 invoice_date 声明为 "date"、金额字段声明为 "number",会在原始读数旁边多出一层确定性的 data.normalized。invoice_no 上的 required: true 意味着值为空或整个缺失时,它会出现在复核清单里,而不是以空字符串悄悄通过。pattern 是你自己的编号规则;匹配方式与 JSON Schema 一样是部分匹配,要校验整个值请用 ^…$ 锚定。

如果你还不知道该声明什么,就不要传 fields,改传 autoFields: true,模型会从文档本身提议一份结构。这适合探索一份陌生的供应商单据;等字段名稳定下来,再把它们移进显式的 fields 数组,让响应结构不再随调用变化。

用显式字段结构调用 POST /ocr/fields
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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/invoices/inv-4471.jpg",
    "imageType": "url",
    "fields": [
      { "name": "vendor", "type": "string" },
      { "name": "invoice_no", "type": "string", "required": true,
        "pattern": "^[A-Z0-9-]+$" },
      { "name": "invoice_date", "type": "date" },
      { "name": "subtotal", "type": "number" },
      { "name": "tax", "type": "number" },
      { "name": "total", "type": "number" },
      { "name": "line_items", "type": "array",
        "children": [
          { "name": "description", "type": "string" },
          { "name": "qty", "type": "number" },
          { "name": "unit_price", "type": "number" }
        ]
      }
    ]
  }'
Why it matters

驼峰命名(camel-case)是规范写法。 参数为 imageType 与 autoFields。旧的下划线命名(snake_case)别名(image_type、auto_fields)仍可使用,但已弃用(deprecated)——新代码请优先使用驼峰命名。

响应结构

调用成功会返回 { status: "success", data: { ... } }。data 分为四块,每块只做一件事:

  • data.values —— 业务数据本身,形状与你声明的 fields 完全一致,不掺杂其他内容。
  • data.cells —— 以路径(total、line_items[0].unit_price)为键的扁平映射。每个 cell 带有基于 0–1000 归一化网格(0,0 = 左上角,1000,1000 = 右下角)的轴对齐矩形 box { xmin, ymin, xmax, ymax }、跟随页面倾斜的四点 quad(所以哪怕是歪着拍的手机照片也能干净地框住),以及 verified、review、evidence。换算成像素以 data.image 为基准:pixel_x = box.xmin / 1000 × data.image.width。
  • data.review —— 汇总信息:unit: "field"、declared / returned / boxed / verified 各项计数、flagged({ path, reasons } 的数组)以及 by_reason 直方图。需要人工关注的条数就是 flagged.length,没有额外的计数器;by_reason 统计的是全部理由而非每条一次,因此其总和会大于等于 flagged.length。
  • data.normalized —— 与 values 同形的稀疏树,只装那些声明了标量类型或 pattern 的字段的确定性解析结果,绝不会覆盖 values。

verified 是判定,而不是字符匹配分数:只要 review 里带有理由(无论哪一类)就是 false;校验跑过且没有任何标记则为 true;本就没有可校验对象时(例如整行的并集)为 null。字符比对本身在 evidence.text_match 里,所以 verified: false 与 text_match: true 同时出现并不矛盾——字符对上了,但被你声明的规则拦下了。evidence 还包含 source(vision_symbol_match、token_id 等)、match_ratio,以及 printed_text(OCR 在该坐标读到的字形)。需要精确字符串比对时,请与 printed_text 对照。

理由代码属于 API 契约词汇,不做翻译:text_mismatch、missing、pattern_mismatch、type_mismatch、out_of_range、low_ratio、overwide_box 等。reasons 始终是数组,按排序排列,第 0 项为主要理由——请为你处理的代码建立映射,并为未知代码保留一条通用提示。

POST /ocr/fields → 响应(节选)
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
{
  "status": "success",
  "data": {
    "values": {
      "vendor": "Acme Supply Co.",
      "invoice_no": "INV-4471",
      "invoice_date": "2026/06/18",
      "subtotal": "1,859",
      "tax": "186",
      "total": "2,045",
      "line_items": [
        { "description": "Steel bracket 40mm", "qty": "12", "unit_price": "98" }
      ]
    },
    "cells": {
      "total": {
        "box": { "xmin": 595, "ymin": 974, "xmax": 781, "ymax": 1000 },
        "quad": [
          { "x": 594, "y": 975 }, { "x": 781, "y": 972 },
          { "x": 781, "y": 998 }, { "x": 595, "y": 1000 }
        ],
        "verified": true,
        "review": null,
        "evidence": {
          "text_match": true,
          "source": "vision_symbol_match",
          "match_ratio": 0.93,
          "printed_text": "2,045"
        },
        "normalized": { "value": 2045, "type": "number", "method": "deterministic" }
      },
      "line_items[0].unit_price": {
        "box": { "xmin": 693, "ymin": 460, "xmax": 738, "ymax": 488 },
        "quad": [
          { "x": 693, "y": 460 }, { "x": 738, "y": 460 },
          { "x": 738, "y": 488 }, { "x": 693, "y": 488 }
        ],
        "verified": false,
        "review": { "reasons": ["text_mismatch"] },
        "evidence": {
          "text_match": false,
          "source": "vision_symbol_match",
          "match_ratio": 0.62,
          "printed_text": "9B"
        }
      }
    },
    "review": {
      "unit": "field",
      "declared": 9,
      "returned": 9,
      "boxed": 9,
      "verified": 8,
      "flagged": [
        { "path": "line_items[0].unit_price", "reasons": ["text_mismatch"] }
      ],
      "by_reason": { "text_mismatch": 1 }
    },
    "normalized": {
      "invoice_no": "INV-4471",
      "invoice_date": "2026-06-18",
      "subtotal": 1859,
      "tax": 186,
      "total": 2045,
      "line_items": [ { "qty": 12, "unit_price": 98 } ]
    },
    "image": { "width": 1654, "height": 2339 }
  }
}
✓ Verified

坐标并非照搬模型给出的文字。 语言模型会返回每个值的文本——以及它用到了哪些词元(word token)的提示——但从不返回框本身。引擎随后会把这段文本逐字符地与视觉 OCR 在页面上实际检测到的符号进行匹配;evidence.match_ratio 就是匹配上的比例,框则落在这些字符真正来源的像素上。模型的词元提示可能带噪声(它有时会在重复的行之间把它们搞混),所以系统用列一致性与行一致性检查来验证它们,而不是盲目采信。这个比例只是佐证,不是判定——判定由 verified 与 review 给出。完整原理见为什么边界框让 OCR 可审计。

会变成复核信号的声明

FieldSpec 除了给字段命名,还能声明“什么样的值才算好”,而每一项声明都对应一个复核理由。required: true 会在值为空或压根没返回时触发 missing——这是字符比对本身永远看不到的唯一一类失败。pattern 触发 pattern_mismatch,enum 在值落到集合之外时同理,min / max 针对归一化后的数字触发 out_of_range,标量 type 在无法解析时触发 type_mismatch。

这些都不会传给模型。声明并不会让抽取更准——值在有无声明的情况下都一样返回。声明决定的是哪些路径会进入 data.review.flagged,以及在 label 的情形下引擎把坐标锚定在哪里。让“读”和“查”彼此独立正是要点所在:唯有如此,两者的一致才具有分量。

真正用来引导模型的是 description:用平实的自然语言说明要抓取什么、怎么抓。而 type: "array" 配上 children,正是抽取重复明细行(line items)的方式——一个子结构对应多行,每一行都可以用 line_items[0]、line_items[1] 这样的路径定位。(我们在从发票中提取明细行一文中对此做了深入讲解。)

声明嵌套明细行的 FieldSpec
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

with open("invoice.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": "Supplier / billing company name"},
            {"name": "invoice_no", "type": "string", "required": True,
             "description": "Invoice number as printed"},
            {"name": "invoice_date", "type": "date"},
            {"name": "total", "type": "number",
             "description": "Grand total"},
            {"name": "line_items", "type": "array",
             "description": "One row per line on the invoice",
             "children": [
                 {"name": "description", "type": "string"},
                 {"name": "qty", "type": "number"},
                 {"name": "unit_price", "type": "number"},
             ]},
        ],
    },
    timeout=200,
)

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

# 印在纸面上的值,以及它旁边确定性的解析结果
print(data["values"]["total"], data["normalized"].get("total"))

# 复核队列:每个需要人工查看的路径一条
for item in data["review"]["flagged"]:
    cell = data["cells"].get(item["path"])
    print(item["path"], item["reasons"][0], cell["box"] if cell else None)
Why it matters

values 是读数,不是逐字节的副本。 印为 7,855 的合计会以字符串 "7,855" 返回——不做摘要也不改写,正因如此这个值才能锚定到坐标上。但它终究是模型读出来的文本,而字符比对会先折叠全角形式、括号与空白再作比较,所以像 (税抜) → (税抜) 这样的改写会通过。需要精确字符串比对时,请与 cells[path].evidence.printed_text(OCR 在该坐标读到的字形)对照。解析后的形态(ISO 日期、去掉分隔符的数字)放在 data.normalized,绝不覆盖 values。你在网页界面里看到的 ¥ 只是装饰,不属于值本身。引擎只接受栅格图像——JPEG、PNG、GIF、BMP、TIFF、WebP——并会自动转换为 RGB。

走异步:批量上传、任务与 Webhook

POST /ocr/fields 是同步的,非常适合在一次请求/响应循环里处理单张发票。它会在读取页面期间保持连接,处理时间上限为 180 秒;超过就会返回 504 与 error.code: "ocr_engine_timeout",且该次调用不计费。触到上限的原因通常是版面密度而非像素数,所以对策是一页一图,或者走下面的异步路径。

如果是一整个文件夹的发票,可以用 POST /upload 把它们提交到一个表格(sheet)里(multipart 形式、可重复的 files,每次请求最多 20 个文件)。默认情况下它会立即返回一个 jobs 数组:

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

之后你有两种方式得知结果:轮询 GET /jobs/{jobId},或者注册一个 webhook。Webhook 是每个空间一个 URL,通过 X-Spaceocr-Signature 头做 HMAC-SHA256 签名。你会关心的事件有 upload.received、item.created、ocr.completed(其 data.result 以同样的 values / cells / review / image 结构携带提取结果)以及 ocr.failed。在信任任何 payload 之前,请务必先验证签名。

幂等性、请求追踪与限流

有几个请求头能让生产流程安全地重试:

Header用途
Idempotency-Key在 /ocr/fields、/create 与 /upload 上受理。用相同的 key 重发会在 24 小时内重放缓存的响应(X-Idempotent-Replay: true)——重试安全、不会重复扣费。它是重试保护,不是存储手段。
X-Request-Id每个响应都会返回(req_xxx);记录下来以便排查问题。
X-RateLimit-Remaining该密钥在当前这一分钟内剩余的可调用次数。

限流为 每个密钥 60 次/分钟、每个 uid 600 次/分钟。超出后会返回 429,带 error.code: "rate_limited",等待秒数放在 Retry-After 响应头里——不要立刻重发,请按这个值退避(back off)。

做容量规划时可以参考:生产流量上观测到的耗时约为 p50 7.2 秒、p90 10.5 秒。这是观测分布而非 SLA,字段多、版面密的发票会高于这个区间。

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

从提取到可查询的表格

一旦发票被提取进表格,你就不必为了再读取它们而重跑 OCR。GET /view 会对已存储的行做服务端查询——where、sort、select、limit、offset——不收费、也不会重新提取。每一行都以与直接调用相同的 values / cells / review / image 结构返回;想要更精简的 payload,可以加上 boxes=0 去掉 cells 映射。从那里你还能导出为 CSV(UTF-8 BOM,所以 Excel 和中日韩文本都能正常打开)——参见把扫描文档转成 CSV。

丢入一张发票,强类型字段便自动填好——和 API 返回的是同一份数据,只是在界面里呈现。

定价

POST /ocr/fields 每次调用收费 $0.05,POST /upload 为 $0.05 × N 张图片。失败不收费——400 invalid_image 与 504 ocr_engine_timeout 根本不会走到计费环节,而 502 引擎错误或 ocr.failed 事件会自动退款。只读接口(GET /space、/view、/jobs、/amount、/health)免费。免费档为每月 100 点数、无需信用卡;付费档从 Starter 每月 $19 起,Pro 为每月 $39——最新档位请见定价页。

在已提取的发票中搜索,直接跳到匹配的单元格——以及它的来源框。

如何用 API 从发票中提取数据

  1. 获取 API 密钥
    登录后生成一个以 spocr_ 为前缀的密钥。每个请求都通过 Authorization: Bearer 头向 https://api.space-ocr.com 鉴权。
  2. 准备发票图片
    引擎只读栅格图像——JPEG、PNG、GIF、BMP、TIFF、WebP。提供一个公开可访问的 URL,或把图片编码为纯 base64,并把必填参数 imageType 设为 'url' 或 'base64'。
  3. 声明你要的字段
    向 /ocr/fields 发送 POST 请求,传入 fields[] 数组:vendor、invoice_no、invoice_date、各金额字段,以及作为数组并带 children 的 line_items。若还不知道结构,就改传 autoFields: true。
  4. 读取值与复核队列
    业务数据取自 data.values,然后遍历 data.review.flagged。每条记录是一个路径加上它的理由;用该路径在 data.cells 中查出对应的 box、quad 与 evidence。
  5. 扩展到批量与查询
    如需处理多张发票,用 POST /upload 把它们提交到一个表格,再通过轮询 GET /jobs/{jobId} 或 ocr.completed webhook 获取结果。之后用 GET /view 查询已存储的行,或导出为 CSV。
提取发票数据,哪个 API 最好?
一个好的发票提取 API 应当能读懂任意版式,返回干净的强类型字段,并为每个值提供溯源信息。space-ocr 的 POST /ocr/fields 一次同步调用就能做到:声明一份 fields[] 结构,或者传 autoFields: true 让模型来提议,每个值都会出现在 data.values 里,并在 data.cells 的同一路径上带有轴对齐的 box、随页面倾斜的 quad、verified 判定及其佐证;需要人工查看的路径则列在 data.review.flagged 中。
除了表头字段,我能提取发票明细行吗?
可以。使用 type 为 'array' 的 FieldSpec,并配上描述单行结构的 children 模式(例如 description、qty、unit_price)。每一行都能用 line_items[0].unit_price 这样的路径定位,并在 data.cells 中拥有自己的 box 与 quad 坐标。供应商、发票号、日期、合计等表头字段会在同一次调用中一并提取。
发票提取 API 接受 PDF 吗?
引擎只接受栅格图像——JPEG、PNG、GIF、BMP、TIFF、WebP——并会自动转换为 RGB。把图片以 URL 或纯 base64 的形式放进 'image' 字段;imageType 是必填参数,需要设为 'url' 或 'base64'。
发票提取 API 的错误和限流是怎么处理的?
限流为每个密钥 60 次/分钟、每个 uid 600 次/分钟。超出后返回 HTTP 429,带 error.code 'rate_limited',等待秒数放在 Retry-After 响应头里。密钥缺失或无效返回 401;403 表示密钥有效但资源不在其范围内。在 /ocr/fields、/create 与 /upload 上使用 Idempotency-Key,这样重试会重放 24 小时内缓存的响应,而不会重复扣费。
从一张发票提取数据要花多少钱?
POST /ocr/fields 每次调用收费 $0.05,/upload 为每张图片 $0.05。失败不收费:400 invalid_image 与 504 超时根本不会走到计费环节,502 引擎错误或 ocr.failed 事件会自动退款。免费档每月包含 100 点数、无需信用卡;Starter 为每月 $19,Pro 为每月 $39。

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

免费档——每月 100 点数,无需信用卡。每个字段都附带其在页面上的位置。

相关