银行卡识别 API:一张照片读出卡号、BIN 与发卡行

银行卡识别 API:一张照片读出卡号、BIN 与发卡行

支付绑卡、钱包开户、商户进件、报销打款账户采集、代发工资卡号录入,第一件事都是让用户把 16 ~ 19 位卡号敲进输入框。手输一串长数字,错一位就打款失败,很多人要念两遍、核两遍;用传统 OCR 方案,又常被凸字反光、卡面花纹、斜拍、手指遮挡卡住。bankcard.ocr 把这件事做成一个 POST 接口:一张照片进去,卡号、卡号前 6 位 BIN、发卡行、卡类型、卡组织、卡等级、卡名称、持卡人姓名与有效期一起结构化出来 ,附带整体置信度 和图片质量提示。

一个必须提前讲清楚的计费口径:本接口调用即计费,只要图片通过校验进入识别流程,无论最终识别成功还是失败都计一次费用;只有"没传图片 / 图片超标 / 格式不支持"这类参数级错误才不收费。详见第九节。

  • api.xujian.tech
  • V: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"。涉及真实绑卡与打款,请以支付通道的鉴权或打款验证为准,接口结果只作为录入提速手段。

七、实践建议

  1. 引导用户拍卡号面 。只传背面(签名条、客服电话)时 cardNo 通常为空,前端最好在拍照页给出示意框与示例图。
  2. 先看 confidence 和 qualityNote 再入库。低于 90 分或有质量提示的,走人工复核队列,别直接落库。
  3. 空串不等于识别失败 。cardLevel、cardName、holderName、expiryDate 在借记卡上常常就是空串,按"卡面没印"处理即可。
  4. 卡号一定要做 Luhn 兜底。接口返回的是卡面原文,加一道校验位能挡掉绝大多数串行错误。
  5. 别传缩略图。卡号字高很小,分辨率不足最容易既扣费又识别不出;建议短边不低于 800px。
  6. 调用即计费,重试要谨慎。同一张失败的照片连续重试只会多扣费,先解决图片质量问题(重新拍/去反光/挪开手指)再调。
  7. 超时设 90 秒以上。含远程图片下载(最长 15 秒)与识别,别用 5 秒、10 秒的常规超时。
  8. 批量建档做去重。同一张照片按文件哈希去重,避免重复计费;同一卡号的识别结果缓存 7 ~ 30 天。
  9. 别用 imageUrl 传需要鉴权的地址。服务端是匿名下载,带签名或 Cookie 的地址会下载失败(返回失败且已计费)。
  10. 隐私与合规 。卡号属于敏感金融信息,建议加密存储、展示时用 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 元/次)用于机动车行驶证识别。

相关推荐
Eric.461 小时前
8G 显存极限优化|SDXL+ComfyUI 本地部署 AI 漫剧批量生产工作流(源码 + 报错根治)
人工智能·ai绘画·comfyui·本地部署·ai漫剧
陌上花开缓缓归以1 小时前
mebdlts3.4.1安全启动全流程
服务器·算法
Nebula_g1 小时前
JavaSE拓展:可变参数
java·开发语言·算法·安全·javase·可变参数
司南不思南1 小时前
一次注意力计算到底长什么样?——从 QKV 投影到 KV Cache 的完整拆解
人工智能
涓涓5271 小时前
物资领用无度?管家通物资出入库把用卡标准
人工智能
weixin_750330231 小时前
AI获客技术选型:基于OPC架构的智能营销方案实践
人工智能·架构·ai获客
用户721746588261 小时前
三层时间结构与静音间隔:把 ASR 词级毫秒时间戳变成 SRT 字幕轴
人工智能·ai编程
ADark2 小时前
FDE 入门 · 05|进场第一周:别急着开会,也别催权限
人工智能