银行卡识别 API:一张照片读出卡号、BIN 与发卡行
支付绑卡、钱包开户、商户进件、报销打款账户采集、代发工资卡号录入,第一件事都是让用户把 16 ~ 19 位卡号敲进输入框。手输一串长数字,错一位就打款失败,很多人要念两遍、核两遍;用传统 OCR 方案,又常被凸字反光、卡面花纹、斜拍、手指遮挡卡住。bankcard.ocr 把这件事做成一个 POST 接口:一张照片进去,卡号、卡号前 6 位 BIN、发卡行、卡类型、卡组织、卡等级、卡名称、持卡人姓名与有效期一起结构化出来 ,附带整体置信度 和图片质量提示。
一个必须提前讲清楚的计费口径:本接口调用即计费,只要图片通过校验进入识别流程,无论最终识别成功还是失败都计一次费用;只有"没传图片 / 图片超标 / 格式不支持"这类参数级错误才不收费。详见第九节。
api.xujian.techV:xujian_cq
接口速览
| 关键事实 | 说明 |
|---|---|
| 接口地址 | https://api.xujian.tech/openapi/bankcard/ocr |
| 接口编码 | bankcard.ocr |
| 请求方式 | POST,三种传图方式任选其一 |
| 鉴权方式 | 请求头 X-API-Key,不做签名、时间戳或加密 |
| 传图方式 | ① multipart/form-data 的 file 字段 ② JSON 的 image(base64) ③ imageUrl(远程地址) |
| 图片限制 | 单张 ≤ 10MB ,JPG / PNG / WEBP / BMP / GIF(按文件头识别,不认扩展名) |
| 返回核心字段 | cardNo 卡号、binNo 前 6 位 BIN、bankName 发卡行、cardType 卡类型、cardScheme 卡组织 |
| 计费方式 | 按次计费,0.05 元/次,调用即计费,识别失败同样扣费 |
| 不计费场景 | 未传图片、超过 10MB、格式不支持、服务未启用或未配置、鉴权失败、余额不足 |
| 建议超时 | 90 秒以上(含图片下载与识别,服务端最长 90 秒) |
| 单次耗时 | 通常数秒,costMs 为真实耗时(示例 3120) |
一、为什么银行卡不只是"把卡号读出来"
银行卡看起来是最简单的卡证------就一串数字。但真实场景里它恰恰是最容易翻车的一种:
| 现实情况 | 传统 OCR 的表现 | 本接口的表现 |
|---|---|---|
| 凸字卡号 + 斜向反光 | 数字笔画断裂,3 读成 8、0 读成 6 |
逐位核对,反光区读不准时如实留空并给出提示,不会猜一位凑满长度 |
| 手指压在卡号上 | 截断成两段,位数不对 | 遮挡时留空并在 qualityNote 说明"卡号被手指遮挡" |
卡号按 4-4-4-4 分组印刷 |
分组空格被当成字符,或整段串行 | 只返回数字,空格与横线全部去掉 |
| 卡面花纹、渐变底、异形卡、竖版卡 | 定位框对不上,直接识别失败 | 不做坐标假设,按语义理解卡面版式 |
| 双币卡、单标卡、卡组织标识在右上角 | 需要额外模板才能认卡组织 | cardScheme 直接给出银联 / VISA / MasterCard 等取值 |
| 只返回一堆文本行 | 还得自己写正则找出"哪一行是卡号",还要自己推导 BIN | 直接给出 13 个结构化字段,BIN 由服务端按卡号推导,不靠猜 |
| 识别错了没有提示 | 错卡号直接入库,打款才发现 | 返回 confidence 整体置信度 + qualityNote 图片质量提示,低置信走人工复核 |
一句话概括:传统 OCR 给你"图上的字",本接口给你"能直接用于绑卡的字段"------省掉的是字段归位、数字清洗、BIN 推导、异常判断四件最费人的事。
二、请求参数
2.1 请求头
| 参数名 | 必填 | 说明 |
|---|---|---|
X-API-Key |
是 | 开发者 API Key,缺失或无效直接返回失败(不计费) |
Content-Type |
视传法而定 | 传文件时 multipart/form-data(一般客户端自动带);传 JSON 时 application/json |
2.2 业务参数(三选一,优先级从高到低)
| 参数名 | 位置 | 类型 | 说明 |
|---|---|---|---|
file |
multipart 表单文件字段 | File | 图片文件,字段名固定为 file,最常用 |
image |
JSON body / 表单 / Query | String | 图片 base64 内容,允许带 data:image/jpeg;base64, 前缀 |
imageUrl |
JSON body / 表单 / Query | String | 图片远程地址,服务端去下载(≤ 10MB,下载超时 15 秒) |
三种方式任选其一即可。同时传时优先级为
file>image>imageUrl。格式校验按文件头字节 判断,改扩展名(把
.png改成.jpg)骗不过去,伪造的"图片"会直接返回"不支持的图片格式"且不收费。
三、返回字段
3.1 顶层与 data
| 字段 | 类型 | 说明 |
|---|---|---|
code |
int | 0 成功,非 0 失败(统一为 500) |
msg |
String | 成功为 success,失败为具体原因(已计费的失败会带"(本次调用已计费)") |
data |
Object | 业务数据,失败时为 null |
3.2 卡号与发卡行字段
| 字段 | 类型 | 示例 | 说明 |
|---|---|---|---|
side |
String | front | 版面:front 卡号面 / back 背面 / unknown 无法判断 |
sideText |
String | 卡号面 | 版面中文名,直接展示用 |
cardNo |
String | 6222021234567890123 | 银行卡号,只保留数字,卡面印刷的空格与横线已去掉 |
binNo |
String | 622202 | 卡号前 6 位 BIN,由服务端按 cardNo 推导,不依赖识别结果本身;卡号不足 6 位时为空串 |
bankName |
String | 中国工商银行 | 发卡行名称 |
cardType |
String | 借记卡 | 卡类型:借记卡 / 信用卡 / 准贷记卡;卡面看不出时为空串 |
cardScheme |
String | 银联 | 卡组织:银联 / VISA / MasterCard / JCB / American Express;看不出时为空串 |
cardLevel |
String | 金卡 | 卡等级:普卡 / 金卡 / 白金卡 / 钻石卡;很多卡面不印等级,此时正常返回空串 |
cardName |
String | 牡丹灵通卡 | 卡名称 |
holderName |
String | ZHANG SAN | 持卡人姓名,卡面印有中文姓名或拼音时返回(拼音保持大写) |
expiryDate |
String | 08/29 | 有效期,格式 MM/YY;借记卡没有此项,返回空串 |
3.3 置信度与尾部字段
| 字段 | 类型 | 示例 | 说明 |
|---|---|---|---|
confidence |
BigDecimal | 97.00 | 整体识别置信度 0 ~ 100,保留 2 位小数;未能评估时为 null |
qualityNote |
String | 卡号被手指遮挡,建议重新拍摄 | 图片质量与风险提示,20 字以内;图片正常时为空串 |
apiCode |
String | bankcard.ocr | 接口编码 |
apiName |
String | 银行卡识别 | 接口名称 |
chargeType |
String | PER_CALL | 计费类型 |
balance |
BigDecimal | 99.9500 | 扣费后的账户余额(元) |
costMs |
Long | 3120 | 本次调用耗时(毫秒) |
字段读不出来时返回空串
"",而不是猜测值 。这一点对银行卡尤其重要:卡号宁可留空也不凑数 ------一个凑出来的 19 位卡号比空串危险得多。
cardLevel、cardName、holderName这几个字段大量卡面本来就不印(尤其是借记卡),返回空串属于正常情况,不要当成识别失败。
四、调用示例
4.1 curl(multipart 上传文件,最常用)
bash
curl -s -X POST "https://api.xujian.tech/openapi/bankcard/ocr" \
-H "X-API-Key: 你的APIKey" \
-F "file=@/path/to/bankcard.jpg"
4.2 curl(JSON 传 base64)
bash
BASE64=$(base64 -w 0 /path/to/bankcard.jpg)
curl -s -X POST "https://api.xujian.tech/openapi/bankcard/ocr" \
-H "X-API-Key: 你的APIKey" \
-H "Content-Type: application/json" \
-d "{\"image\":\"$BASE64\"}"
4.3 curl(传远程地址)
bash
curl -s -X POST "https://api.xujian.tech/openapi/bankcard/ocr" \
-H "X-API-Key: 你的APIKey" \
-H "Content-Type: application/json" \
-d '{"imageUrl":"https://your-domain.com/bankcard.jpg"}'
4.4 Java(Hutool,multipart 上传)
java
import cn.hutool.http.HttpRequest;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import java.io.File;
public class BankCardClient {
private static final String API_URL = "https://api.xujian.tech/openapi/bankcard/ocr";
/**
* 识别银行卡
*
* @param apiKey 开发者 API Key
* @param image 银行卡照片(建议拍有卡号的一面),≤ 10MB
* @return data 节点;识别失败返回 null(注意:已扣费,见 msg)
*/
public static JSONObject recognize(String apiKey, File image) {
String body = HttpRequest.post(API_URL)
.header("X-API-Key", apiKey)
.form("file", image)
.timeout(90000)
.execute().body();
JSONObject json = JSONUtil.parseObj(body);
if (json.getInt("code") == null || json.getInt("code") != 0) {
System.out.println("识别失败:" + json.getStr("msg"));
return null;
}
return json.getJSONObject("data");
}
public static void main(String[] args) {
JSONObject data = recognize("你的APIKey", new File("bankcard.jpg"));
if (data == null) {
return;
}
System.out.printf("卡号=%s BIN=%s 发卡行=%s 卡类型=%s 卡组织=%s 置信度=%s%n",
data.getStr("cardNo"), data.getStr("binNo"), data.getStr("bankName"),
data.getStr("cardType"), data.getStr("cardScheme"), data.getBigDecimal("confidence"));
if (cn.hutool.core.util.StrUtil.isNotBlank(data.getStr("qualityNote"))) {
System.out.println("质量提示:" + data.getStr("qualityNote"));
}
}
}
4.5 Python(base64 方式)
python
import base64
import requests
def recognize_file(api_key: str, path: str):
"""multipart 上传;识别失败返回 None(注意:已扣费)"""
with open(path, "rb") as f:
resp = requests.post(
"https://api.xujian.tech/openapi/bankcard/ocr",
files={"file": f},
headers={"X-API-Key": api_key},
timeout=90,
)
result = resp.json()
if result.get("code") != 0:
print("识别失败:", result.get("msg"))
return None
return result["data"]
def recognize_base64(api_key: str, path: str):
"""JSON base64 上传(服务端只认 JSON 场景时用)"""
with open(path, "rb") as f:
b64 = base64.b64encode(f.read()).decode()
resp = requests.post(
"https://api.xujian.tech/openapi/bankcard/ocr",
json={"image": b64},
headers={"X-API-Key": api_key},
timeout=90,
)
result = resp.json()
return result["data"] if result.get("code") == 0 else None
if __name__ == "__main__":
data = recognize_file("你的APIKey", "bankcard.jpg")
if data:
print(data["cardNo"], data["bankName"], data["confidence"])
4.6 JavaScript(浏览器里选图上传)
javascript
async function recognizeBankCard(apiKey, file) {
const form = new FormData();
form.append("file", file); // file 来自 <input type="file">
const resp = await fetch("https://api.xujian.tech/openapi/bankcard/ocr", {
method: "POST",
headers: { "X-API-Key": apiKey }, // 不要手动设 Content-Type,浏览器会带 boundary
body: form
});
const result = await resp.json();
if (result.code !== 0) {
throw new Error(result.msg);
}
return result.data;
}
五、返回示例
借记卡识别成功(cardLevel、expiryDate 为空串属于正常,卡面本来就没印):
json
{
"code": 0,
"msg": "success",
"data": {
"side": "front",
"sideText": "卡号面",
"cardNo": "6222021234567890123",
"binNo": "622202",
"bankName": "中国工商银行",
"cardType": "借记卡",
"cardScheme": "银联",
"cardLevel": "",
"cardName": "牡丹灵通卡",
"holderName": "",
"expiryDate": "",
"confidence": 97.00,
"qualityNote": "",
"apiCode": "bankcard.ocr",
"apiName": "银行卡识别",
"chargeType": "PER_CALL",
"balance": 99.9500,
"costMs": 3120
}
}
信用卡识别成功(带有效期、持卡人拼音与卡等级):
json
{
"code": 0,
"msg": "success",
"data": {
"side": "front",
"sideText": "卡号面",
"cardNo": "5187101234567890",
"binNo": "518710",
"bankName": "招商银行",
"cardType": "信用卡",
"cardScheme": "MasterCard",
"cardLevel": "金卡",
"cardName": "",
"holderName": "ZHANG SAN",
"expiryDate": "08/29",
"confidence": 96.50,
"qualityNote": "",
"apiCode": "bankcard.ocr",
"apiName": "银行卡识别",
"chargeType": "PER_CALL",
"balance": 99.9000,
"costMs": 3480
}
}
卡号被遮挡(能确认的照常返回,卡号留空并提示,已扣费):
json
{
"code": 500,
"msg": "未能识别出银行卡上的卡号,请上传更清晰、无遮挡的银行卡照片(本次调用已计费)",
"data": null
}
没传图片或图片超标(不收费):
json
{
"code": 500,
"msg": "请上传图片:multipart 的 file 字段、JSON 的 image(base64)、imageUrl(图片地址)三选一",
"data": null
}
六、可直接复用的两段代码
6.1 入库前的质量闸门:低置信度与空卡号转人工
python
def need_manual(data: dict, min_conf: float = 90.0) -> tuple:
"""返回 (是否需要人工, 原因):卡号缺失、位数异常、置信度低、有质量提示时转人工"""
card_no = data.get("cardNo", "")
if not card_no:
return True, "未识别出卡号"
if not 12 <= len(card_no) <= 19:
return True, f"卡号位数异常({len(card_no)} 位):{card_no}"
conf = data.get("confidence")
if conf is None or conf < min_conf:
return True, f"置信度偏低:{conf}"
if data.get("qualityNote"):
return True, "图片质量提示:" + data["qualityNote"]
if data.get("cardType") == "信用卡" and not data.get("expiryDate"):
return True, "信用卡未识别出有效期"
return False, ""
def to_record(data: dict) -> dict:
"""转成业务库记录:空串统一转 None,避免把 '' 写进数据库"""
return {k: (v if v not in ("", None) else None) for k, v in data.items()}
6.2 Luhn 校验、卡号脱敏与 BIN 判定
python
def luhn_ok(card_no: str) -> bool:
"""卡号校验位自检(Luhn 算法);接口不做校验位推断,只返回卡面原文"""
if not card_no.isdigit():
return False
digits = [int(c) for c in card_no][::-1]
total = sum(digits[0::2]) + sum(sum(divmod(d * 2, 10)) for d in digits[1::2])
return total % 10 == 0
def mask(card_no: str) -> str:
"""展示脱敏:保留前 6 位(BIN)与后 4 位,中间打码"""
return card_no if len(card_no) <= 10 else card_no[:6] + "*" * (len(card_no) - 10) + card_no[-4:]
def scheme_of(card_no: str, card_scheme: str = "") -> str:
"""卡组织判定:优先用接口返回,取不到时按卡号首位兜底"""
if card_scheme:
return card_scheme
if card_no.startswith("4"):
return "VISA"
if card_no[:2] in {"51", "52", "53", "54", "55"} or 2221 <= int(card_no[:4] or 0) <= 2720:
return "MasterCard"
if card_no.startswith(("34", "37")):
return "American Express"
if card_no.startswith("35"):
return "JCB"
if card_no.startswith(("62", "81", "60")):
return "银联"
return "未知"
识别引擎不会臆造卡号,但**"卡面原文 + 校验位"是两回事**:Luhn 能挡掉绝大多数串行、跳位错误,却挡不住"伪造的卡号但恰好通过 Luhn"。涉及真实绑卡与打款,请以支付通道的鉴权或打款验证为准,接口结果只作为录入提速手段。
七、实践建议
- 引导用户拍卡号面 。只传背面(签名条、客服电话)时
cardNo通常为空,前端最好在拍照页给出示意框与示例图。 - 先看
confidence和qualityNote再入库。低于 90 分或有质量提示的,走人工复核队列,别直接落库。 - 空串不等于识别失败 。
cardLevel、cardName、holderName、expiryDate在借记卡上常常就是空串,按"卡面没印"处理即可。 - 卡号一定要做 Luhn 兜底。接口返回的是卡面原文,加一道校验位能挡掉绝大多数串行错误。
- 别传缩略图。卡号字高很小,分辨率不足最容易既扣费又识别不出;建议短边不低于 800px。
- 调用即计费,重试要谨慎。同一张失败的照片连续重试只会多扣费,先解决图片质量问题(重新拍/去反光/挪开手指)再调。
- 超时设 90 秒以上。含远程图片下载(最长 15 秒)与识别,别用 5 秒、10 秒的常规超时。
- 批量建档做去重。同一张照片按文件哈希去重,避免重复计费;同一卡号的识别结果缓存 7 ~ 30 天。
- 别用
imageUrl传需要鉴权的地址。服务端是匿名下载,带签名或 Cookie 的地址会下载失败(返回失败且已计费)。 - 隐私与合规 。卡号属于敏感金融信息,建议加密存储、展示时用
mask()脱敏、日志里只留 BIN 与后四位;本接口不落库图片与识别结果,用完即弃。
八、错误码与排查
| code | msg | 是否扣费 | 处理建议 |
|---|---|---|---|
| 0 | success | 是(0.05 元) | 读取 data,先看 confidence / qualityNote |
| 500 | 缺少请求头 X-API-Key | 否 | 请求头补 X-API-Key |
| 500 | API Key 无效 / API Key 已停用 | 否 | 到控制台核对 Key 状态 |
| 500 | 客户不存在或已停用 | 否 | 联系平台开通 |
| 500 | 接口不存在或已停用 | 否 | 确认接口编码 bankcard.ocr |
| 500 | 余额不足,请先充值 | 否 | 预鉴权阶段被拦下,不产生费用 |
| 500 | 银行卡识别服务未启用或未配置,本次调用不计费 | 否 | 平台侧问题,稍后重试或提交工单 |
| 500 | 请上传图片:multipart 的 file 字段、JSON 的 image(base64)、imageUrl(图片地址)三选一 |
否 | 检查字段名与传参方式 |
| 500 | 图片大小不能超过 10MB | 否 | 压缩或裁切后重试 |
| 500 | 不支持的图片格式,仅支持 JPG / PNG / WEBP / BMP / GIF | 否 | 转码成 JPG,注意是按文件头判断,改扩展名无效 |
| 500 | image 不是合法的 base64 内容 / 图片内容为空 | 否 | 检查 base64 是否完整、是否被 URL 转义 |
| 500 | 图片地址访问失败(HTTP 404)/ 图片地址下载失败或超时 / 图片地址返回内容为空 | 否 | 换成可匿名访问的外链 |
| 500 | 未能识别出银行卡信息,请上传清晰的银行卡卡号面照片(本次调用已计费) | 是 | 确认图片确实是银行卡,重新拍摄后再调 |
| 500 | 未能识别出银行卡上的卡号,请上传更清晰、无遮挡的银行卡照片(本次调用已计费) | 是 | 提高清晰度、挪开手指、避开反光 |
| 500 | 银行卡识别失败(...)(本次调用已计费) | 是 | 上游超时等异常,确认图片没问题后可重试 |
判断是否真的扣费,最可靠的依据是失败响应里是否带"(本次调用已计费)",以及上一次成功响应的
data.balance变化。
九、计费与接入
| 项目 | 说明 |
|---|---|
| 单价 | 0.05 元/次 |
| 计费方式 | 按次计费,调用即计费:图片通过大小与格式校验、进入识别流程后即计一次 |
| 计费判定 | 识别成功计一次;识别失败(图片模糊、不是银行卡、卡号认不出、上游超时)同样计一次 |
| 不计费场景 | 未传图片、超过 10MB、格式不支持、base64 非法、远程地址不可访问、服务未启用或未配置、鉴权失败、余额不足 |
| 建议超时 | 90 秒以上 |
为什么识别失败也扣费:图片一旦通过校验进入识别流程,算力就已经发生,与最终是否抽得出字段无关。这也是本接口与其他接口(评估不出不收费、审核不可用不收费)最大的差别,接入时请在前端明确提示用户"识别失败也会消耗一次调用"。
接入流程:注册开发者账号 → 控制台创建 API Key → 请求头带上 X-API-Key 即可调用,无需签名或加密。控制台可查看调用量、扣费流水与余额。
服务站点:
api.xujian.tech(纯文本域名,不做跳转)。接口试用、数据与充值咨询可在控制台提交工单,或联系 Vxujian_cq。
边界说明:本接口做的是卡面信息结构化(把照片上的字变成字段),不做卡号真实性校验、不做持卡人身份核验、不查卡片状态。涉及真实绑卡与打款,请以支付通道的鉴权结果为准。
十、小结
银行卡录入这件事,手输慢、易错、体验差,传统 OCR 又在凸字反光和花纹底面前格外脆弱。做成接口之后,成本变成每次五分,并且连"这张图能不能读、卡号读没读全"都一并告诉你。
使用时要记住:
- 三种传法任选:文件、base64、远程地址,业务系统里最省事的永远是 multipart 直传;
- 先读
confidence+qualityNote再入库:低分与有提示的走人工; binNo是服务端推导的:只要卡号识别正确,BIN 就一定正确,可直接用于判定发卡行与卡种;- 卡号拿不准返回空串,绝不凑数:配合 Luhn 兜底,把风险挡在入库之前;
- 调用即计费,失败也扣一次:重试前先解决图片质量问题,别拿重试当重试策略。
配套接口:idcard.ocr(0.05 元/次)识别身份证,二者组合即可覆盖"实名 + 绑卡"的完整录入链路;vehicle.license(0.1 元/次)用于机动车行驶证识别。