开发者/功能接入
商品与下单
本篇按采购流程依次说明:查余额、找商品、看价格、下单,最后获取卡密。
相关文档:认证与签名 · 通用约定 · Webhook 通知
#账户与余额
#账户信息
GET/api/v1/account 返回你的企业信息,以及 API 是否已开通。
{
"merchant_id": "M00000001",
"name": "Acme Shop",
"legal_name": "Acme Shop Ltd",
"billing_email": "[email protected]",
"status": "active",
"kyb_status": "approved",
"api_access_enabled": true,
"default_currency": "USD",
"purchase_limits": {
"currency": "USD",
"verified": true,
"max_order_amount": "1000.00",
"daily_order_amount_limit": "10000.00",
"daily_order_count_limit": 1000,
"used_today_amount": "120.50",
"used_today_count": 3,
"day_resets_at": "2026-10-01T00:00:00+00:00"
}
}default_currency是你钱包的币种,所有采购价都以它结算。- CardV 审核通过你的企业资料后,
api_access_enabled变为true。
#余额
GET/api/v1/balance 返回你当前能用多少钱。
{
"currency": "USD",
"balance": "1520.40",
"reserved_amount": "0.00",
"available_balance": "1520.40",
"low_balance_threshold": "200.00",
"low_balance_notified_at": null,
"is_active": true
}available_balance是现在可用的金额,等于balance减去reserved_amount(冻结金额)。- 订单金额超过
available_balance时会被拒绝,不会扣任何钱。 low_balance_threshold是余额提醒的阈值,低于它会发邮件提醒你,在商户后台设置。- 加款请在商户后台操作。
#汇率
GET/api/v1/fx-rates?quote=EUR 返回 USD 到其他币种的汇率。
如果你的店铺不是用 USD 标价,可以用它换算售价。CardV 始终按 USD 从你的钱包扣款,这个汇率只用于你自己定价。
{
"base": "USD",
"quote": "EUR",
"rate": "0.9234",
"as_of": "2026-10-11T07:05:00Z"
}quote是 ISO 4217 币种代码,例如EUR、GBP、JPY。base可不传,只支持USD。rate表示 1 USD 可换多少quote币种,是十进制字符串,不要按浮点数解析。as_of是 CardV 最近一次更新该汇率的时间。汇率每小时更新一次。quote=USD返回"rate": "1"。- 请在你这边保存汇率,每天查询几次就够了。
| 状态 | 含义 | 怎么处理 |
|---|---|---|
| 400 | 缺少 quote、不是 3 位字母代码,或 base 不是 USD | 修正请求 |
| 404 | No exchange rate for this currency. | 这个币种请自行定价 |
| 503 | Exchange rate temporarily unavailable. | 继续使用上次保存的汇率,稍后重试 |
这个请求和其他请求一样要签名。签名用的路径包含查询字符串,例如 /api/v1/fx-rates?quote=EUR。
#商品
SKU 就是一个可以购买的具体商品,例如"Steam 钱包 10 美元"。它的编号形如 S000456,报价和下单都针对 SKU。
#商品类型
每个 SKU 都属于下面其中一种类型。用 denomination_type 和 product_type 区分。
| 类型 | 如何识别 | 价格 | 下单 | 你收到什么 |
|---|---|---|---|---|
| 固定面值 | denomination_type: "fixed" | merchant_price 就是一件的单价。 | 传 quantity。 | deliveries 里的卡密。 |
| 自选面值 | denomination_type: "range" | merchant_price_min 到 merchant_price_max。实际价格取决于金额,请询价。 | 传 quantity 和 amount。 | deliveries 里的卡密。 |
| 直充 | product_type: "direct_charge" | 与固定面值相同。 | 还要传 inputs(见直充商品)。 | 通常没有卡密。succeeded 表示账户已充值到账。 |
| 话费充值 | 不在这个目录里 | 单独询价 | 单独的 API | 见话费充值。 |
vertical是产品线,不是商品类型。直充商品也可能是vertical: "gift_card"。- 任何固定面值或自选面值 SKU 都可能以 CardV 兑换链接 代替卡密交付,这时 SKU 的
delivery_mode: "voucher"。你收到的是一个链接(delivery_type: "link",链接在redeem_url里),客户打开它即可拿到卡密。像卡密一样发给你的客户即可。
GET/api/v1/skus 列出你可以购买的 SKU。示例:
GET /api/v1/skus?search=steam®ion=US&limit=50&offset=0筛选条件(都是可选的):
| 参数 | 示例 | 匹配内容 |
|---|---|---|
search | steam | SKU 编号、名称或品牌 |
brand | Steam | 品牌名(不区分大小写) |
region | US | 国家代码或国家名称;GLC 表示全球,EU 表示欧洲 |
category | Travel | 业务分类,例如 Travel、Digital Wallets & Payment、Game Credits |
vertical | gift_card | 商品线 |
product_type | pin_code | 交付方式 |
分页:传 limit(默认 100,最大 500)和 offset。不断增大 offset 继续请求,直到 offset 达到 count。
{
"count": 7,
"limit": 2,
"results": [
{
"sku_id": "S000456",
"product_id": "P000123",
"name": "Steam Wallet 10 USD",
"product_name": "Steam Wallet US",
"brand": "Steam",
"region": "US",
"vertical": "gift_card",
"product_type": "pin_code",
"denomination_type": "fixed",
"denomination_value": "10",
"face_currency": "USD",
"merchant_price": "9.25",
"merchant_price_min": "9.25",
"merchant_price_max": "9.25",
"settlement_currency": "USD",
"availability": "available",
"min_quantity": 1,
"max_quantity": 100,
"required_input_schema": [],
"...": "more fields"
},
{
"sku_id": "S006126",
"product_id": "P002210",
"name": "Apple ES",
"product_name": "Apple ES",
"brand": "Apple",
"region": "ES",
"vertical": "gift_card",
"product_type": "pin_code",
"denomination_type": "range",
"denomination_value": "0",
"min_face_value": "2",
"max_face_value": "250",
"amount_step": null,
"face_currency": "EUR",
"merchant_price": "2.28",
"merchant_price_min": "2.28",
"merchant_price_max": "284.15",
"price_label": "From 2.28 USD",
"settlement_currency": "USD",
"availability": "available",
"...": "more fields"
}
],
"filter_options": {"brands": [], "regions": [], "categories": [], "verticals": []}
}GET/api/v1/skus/{sku_id} 返回单个 SKU,字段与列表相同。
最常用的字段:
| 字段 | 含义 |
|---|---|
sku_id | 报价和下单时使用的编号。 |
merchant_price | 你的采购单价,币种为 settlement_currency。自选面值(range)SKU 这里只是 min_face_value 那个金额的价格(起步价),不能当作其他金额的单价,请按 amount 询价。 |
availability | available(可售)或 unavailable(不可售)。只能购买可售的 SKU。 |
denomination_type | fixed(固定面值)或 range(自选面值),见固定面值与自选面值。 |
denomination_value | 固定面值 SKU 的面值;自选面值 SKU 为 "0"。 |
min_face_value、max_face_value | 自选面值 SKU:客户可选的最小和最大金额,币种为 face_currency。 |
merchant_price_min、merchant_price_max | 最低和最高金额的价格。fixed SKU 两者都等于 merchant_price。可用来展示价格区间;下单前务必按实际金额询价。暂时无法定价时 merchant_price_max 为 ""。 |
price_label | merchant_price 的展示文本;自选面值 SKU 以 From 开头,例如 From 2.28 USD。 |
face_value_label | 面值的展示文本,例如 10 USD 或 50 - 1000 GBP。 |
discount | 相对面值的折扣,例如 7.50% off face value。仅当 fixed SKU 的卡面币种与你的钱包币种相同时才有,否则为 ""。 |
amount_step | 仅自选面值 SKU。金额必须是该值的整数倍,例如 "1" 表示只能是整数。null 表示没有步长限制。 |
face_currency | 卡面币种,可能和你的钱包币种不同。 |
min_quantity、max_quantity | 一个订单行允许购买的数量范围。 |
product_type | pin_code(交付卡密)或 direct_charge(直接充值到账户)。 |
delivery_type | 供应商交付的形式:code、code_pin、code_serial、url 或 direct_charge。delivery_mode 为 voucher 时,你收到的是链接。 |
delivery_mode | code(直接交付卡密)或 voucher(交付 CardV 兑换链接)。 |
voucher_instructions | 仅 voucher SKU:告诉客户如何使用该链接的说明文字。其他情况为 ""。 |
vertical、category | 产品线和业务分类,用于筛选和展示。 |
region、region_name、region_label | 国家或地区代码、名称和展示文本。 |
required_input_schema | 直充商品需要你提供的信息。 |
brand_logo_url、image_url | CardV 托管的图片地址,没有时为 ""。 |
description、redemption_instructions、terms、disclaimer | 可以直接展示给你客户的说明文字。 |
提示:
- 你只能看到已上架、且对你的账户开放的 SKU。其他 SKU 返回 404。
- 建议每 5–15 分钟同步一次商品列表;下单前务必重新报价。
- 给客户展示价格时,请用你同步下来的列表价格。不要在每次页面浏览或每个访客访问时都调用报价接口。
- 自选面值 SKU 的价格跟随供应商成本,一天内可能变动几次。请在同步 SKU 时一并刷新
merchant_price_min和merchant_price_max。 filter_options列出了可用于筛选的品牌、地区和商品线。
#按商品同步
如果你的商城是“一个商品一页、页内选面值”,请按商品同步,不必逐个 SKU 拉取。
GET/api/v1/products 返回商品列表,每个商品都带着其中你可购买的全部 SKU。
GET /api/v1/products?brand=Steam®ion=GB&limit=50&offset=0筛选和分页参数与 GET/api/v1/skus 相同。count 是商品数,sku_count 是这些商品下的 SKU 总数。
{
"count": 1,
"sku_count": 4,
"limit": 50,
"results": [
{
"product_id": "P000123",
"product_name": "Steam Wallet UK",
"brand": "Steam",
"category": "Gaming",
"region": "GB",
"product_type": "pin_code",
"image_url": "https://b2b.cardv.net/api/v1/catalog-assets/steam.png",
"required_input_schema": [],
"skus": [
{
"sku_id": "S000456",
"name": "Steam Wallet 5 GBP",
"label": "5 GBP",
"denomination_type": "fixed",
"denomination_value": "5",
"face_currency": "GBP",
"merchant_price": "7.61",
"settlement_currency": "USD",
"availability": "available",
"min_quantity": 1,
"max_quantity": 100,
"...": "more fields"
}
]
}
],
"filter_options": {"brands": [], "regions": [], "categories": [], "verticals": []}
}GET/api/v1/products/{product_id} 返回单个商品及其 SKU,另含 description、redemption_instructions、terms 和 disclaimer。
skus里每个 SKU 的字段和取值都与GET/api/v1/skus完全一致,报价和下单照常使用sku_id。- 选项文字请用
label,例如5 GBP或Tinder Plus 1 Month。有些商品是套餐,不是面值。 - 一个商品只有一种面值币种和一种面值类型;范围面值 SKU 一定是单独的商品。
- 列表不含长文本,请按商品从
GET/api/v1/products/{product_id}读取一次。
#固定面值与自选面值
大多数 SKU 是固定面值,例如 10 USD。少数 SKU 是自选面值:由客户在一个范围内自己定金额,例如 5 到 500 USD。
| 类型 | 报价时 | 下单时 |
|---|---|---|
fixed | 传 quantity | 不传 amount |
range | 传 quantity 和 amount | 传 amount |
自选面值 SKU 的 amount 必须在 min_face_value 和 max_face_value 之间,币种为 face_currency。
自选面值 SKU 的价格。 SKU 列表和商品列表里的 merchant_price、price_label 只是 min_face_value 那个金额的价格。merchant_price_min 和 merchant_price_max 是最低和最高金额的价格,中间金额的价格不是其中任何一个的简单倍数。定零售价或下单前,请按实际 amount 询价,并把该报价的 merchant_price 作为 expected_unit_price 传上来。
部分自选面值 SKU 还有 amount_step。不为 null 时,amount 必须是它的整数倍。例如 "amount_step": "1" 时,可以传 25,不能传 25.50。金额不符合步长时,报价或下单都会返回 HTTP 400,不会扣任何钱。
#直充商品
有些商品会直接充值到客户的账户里,例如游戏账号。这类商品需要你提供账户信息,SKU 会列出需要哪些字段:
"required_input_schema": [
{"key": "player_id", "label": "Player ID", "type": "text", "required": true},
{"key": "server", "label": "Server", "type": "text", "required": false}
]下单时,在订单行的 inputs 里按 key 填写对应的值:
"inputs": {"player_id": "123456789", "server": "EU"}- 除非标明
"required": false,否则都是必填字段。type是给你的表单字段的类型提示,例如text。 - 缺少必填值时,订单会被拒绝,并返回
items错误。 - 这些是你客户的个人信息,请妥善保护(见安全)。
#报价
报价用于查询某个数量下的当前价格。
GET /api/v1/skus/S000456/quote?quantity=2
GET /api/v1/skus/S000789/quote?quantity=1&amount=25.00{
"sku_id": "S000456",
"settlement_currency": "USD",
"merchant_price": "9.25",
"quantity": 2,
"total_price": "18.50",
"min_quantity": 1,
"max_quantity": 100,
"availability": "available"
}- 报价不会锁定价格,价格随时可能变化。
- 只在客户结账时报价一次,并设置较短的超时,例如 5 秒。如果报价超时,继续展示同步下来的价格并重试;不要因为报价慢就显示错误页面。
- 为了保护自己,下单时请把报价里的
merchant_price作为expected_unit_price一起传上来。请传报价返回的价格,不要传自己计算的价格:必须完全一致,精确到分。如果价格已经变了,CardV 会拒绝这笔订单,不扣任何钱。 - 数量或金额超出范围,或金额不是
amount_step的整数倍时,返回 HTTP 400,错误字段为quantity或amount。
#下单
POST/api/v1/orders 用于购买一个或多个 SKU,并从钱包扣款。和所有请求一样,这个请求必须签名。
{
"external_order_id": "SHOP-20260929-10001",
"items": [
{"sku_id": "S000456", "quantity": 2, "expected_unit_price": "9.25"},
{"sku_id": "S000789", "amount": "25.00", "expected_unit_price": "23.75"},
{
"sku_id": "S000900",
"expected_unit_price": "4.90",
"inputs": {"player_id": "123456789"}
}
]
}| 字段 | 是否必填 | 说明 |
|---|---|---|
external_order_id | 是 | 商户订单号,1–120 个字符,必须唯一。 |
items | 是 | 一个或多个订单行。 |
items[].sku_id | 是 | 要购买的 SKU。 |
items[].quantity | 否 | 购买数量,默认 1。 |
items[].amount | 自选面值时必填 | 要购买的面值。必须符合 min_face_value、max_face_value 和 amount_step。 |
items[].expected_unit_price | 建议填写 | 报价得到的 merchant_price,请每次都传。 |
items[].inputs | 直充商品必填 | 直充商品的账户信息。 |
CardV 接受订单时,会一次性从钱包扣除订单总额,随后在后台开始发货。
返回 HTTP 201:
{
"idempotent_replay": false,
"order": {
"order_id": "O-00001234",
"external_order_id": "SHOP-20260929-10001",
"status": "accepted",
"total_amount": "46.20",
"...": "more fields"
}
}请保存 order.order_id。下单响应里不含卡密,稍后通过查询订单获取。
#订单被拒绝
订单被拒绝时返回 HTTP 400,不会扣任何钱。根据错误字段判断原因:
| 错误字段 | 原因 | 怎么处理 |
|---|---|---|
items | 价格变了、商品不可售、金额不对或缺少直充信息 | 重新报价,改正后再提交 |
balance | 钱包余额不足 | 在商户后台加款 |
risk | 超过单笔或每日限额 | 联系 CardV |
external_order_id | 这个商户订单号已经用于另一笔不同的订单 | 见安全重试 |
价格变化时的错误示例:
{
"items": "SKU S000456 price changed from 9.25 to 9.41 USD; refresh quote and confirm again."
}你的账户对单笔订单金额、每日采购金额和每日订单数设有上限。
GET/api/v1/account 的 purchase_limits 会列出这些上限,以及你今天已用的额度。
0 表示不限。每日限额在 UTC 00:00 重置。
#安全重试
商户订单号(external_order_id)可以防止重复购买。用同一个商户订单号再次提交同一笔订单,CardV 不会重复扣款,而是直接返回已经存在的那笔订单。
| 你提交的内容 | 返回结果 |
|---|---|
| 新的商户订单号 | HTTP 201,创建新订单并扣款。 |
| 相同的商户订单号 + 相同的订单内容 | HTTP 200,"idempotent_replay": true,返回原订单,不再扣款。 |
| 相同的商户订单号 + 不同的订单内容 | HTTP 400,错误字段为 external_order_id,不做任何处理。 |
"相同的订单内容"是指:订单行数量和顺序一致,每行的 SKU、数量、面值和直充信息都相同。如果传了 expected_unit_price,它必须和第一次下单时的价格一致。
系统会先识别是否为重复提交,再检查余额和价格。所以即使价格在这期间变了,重复提交也总是返回原来那笔订单。
#重试流程
没有收到明确结果时,直接把同一笔订单再提交一次即可。
用商户订单号 R 提交 POST /orders
├─ 201 或 200 → 保存 order_id,完成。
├─ 400 items / balance / risk → 订单没有创建。
│ 处理原因后重新提交,可以继续用 R。
├─ 400 external_order_id → R 已被另一笔不同的订单占用,停下来核查。
├─ 403 签名错误 → 重新签名,提交同样的内容。
├─ 429 → 等待 Retry-After 秒数后,重新签名并提交同样的内容。
└─ 超时、5xx 或连接中断
→ 用同一个 R 再提交一次同样的内容。
返回 201 说明第一次没到达,返回 200 说明第一次已成功。要点:
- 不要因为没收到响应就换一个新的商户订单号。 如果第一次请求其实已经到达,换号就会重复购买。
- 每次重新提交都要生成新的时间戳、随机串和签名,订单内容保持不变。
#查询订单
GET/api/v1/orders/{order_id} 返回订单详情、状态和卡密。
{
"order_id": "O-00001234",
"external_order_id": "SHOP-20260929-10001",
"status": "succeeded",
"currency": "USD",
"total_amount": "18.50",
"created_at": "2026-09-29T08:15:30.123456Z",
"updated_at": "2026-09-29T08:15:41.004211Z",
"items": [
{
"sku_id": "S000456",
"product_name": "Steam Wallet US",
"quantity": 2,
"unit_price": "9.25",
"total_price": "18.50",
"delivery_count": 2,
"deliveries": [{"...": "see Codes below"}]
}
],
"...": "more fields"
}total_amount是订单被接受时实际扣除的金额。items[].unit_price是这笔订单锁定的单价。items[].deliveries里是完整卡密,请把整个响应当作机密数据对待。invoice_url和delivery_file_url分别是发票和卡密 CSV 的下载路径,只能在商户后台打开,用 API Key 访问会返回 HTTP 403。- 响应里还有
id(旧的数字编号,请勿使用)、events(仅供展示的处理记录)以及各订单行的发货进度,对接时可以忽略。 - 订单号不存在时返回 HTTP 404。
#订单状态与卡密
#订单状态
accepted ──► processing ──► succeeded
│
├──► partially_succeeded (部分到货,部分未到货)
│
└──► failed ──► refunded (退款回到钱包)| 状态 | 是否结束 | 怎么处理 |
|---|---|---|
accepted(已接受) | 否 | 等待。已扣款,还没开始发货。 |
processing(处理中) | 否 | 等待。不要重新下单。 |
succeeded(成功) | 是 | 读取卡密,交付给客户。 |
partially_succeeded(部分成功) | 是 | 交付已到货的部分,其余部分稍后退款。 |
failed(失败) | 尚未 | 等待变为 refunded。失败不等于已退款。 |
refunded(已退款) | 是 | 钱已经退回你的钱包。 |
如果不用 Webhook,建议这样轮询:5 秒后查一次,然后 10 秒、30 秒、60 秒,之后每 5 分钟一次,并注意不要超过频率限制。大多数订单几秒内就能完成,个别需要人工处理的可能要几个小时。
#卡密
每个已交付的单位是 items[].deliveries 里的一个对象:
{
"status": "stored",
"delivery_type": "card_pin",
"card_number": "X1234",
"pin_code": "9876",
"redeem_url": "",
"expiry_date": "2027-09-29",
"instructions": "Redeem at ..."
}| 字段 | 含义 |
|---|---|
card_number | 主卡密:卡号、兑换码或 PIN。 |
pin_code | 第二个卡密,例如 PIN 或安全码。没有时为空。 |
redeem_url | 兑换链接。link 类型的交付中,链接本身就是卡密。 |
expiry_date | 卡密可使用的最后一天,格式为 YYYY-MM-DD。"" 表示供应商没有提供有效期,并不表示卡密已过期。 |
- 把所有非空字段都交付给客户:
card_number、pin_code、redeem_url、expiry_date和instructions。 delivery_type表示交付形式:code、card_pin、link、code_link或qr。redeem_url要像卡密一样保密。- 不要因为
expiry_date为空就删除、隐藏或作废卡密。只有expiry_date不为空时,才向客户展示有效期。 status为voided(已作废)的单位不要交付给客户。- 直充商品通常没有交付内容,状态为
succeeded即表示已充值到账。 - Webhook 通知里不含卡密,收到通知后请再查询一次订单。
接入遇到问题?请发送邮件至 [email protected],并附上 Merchant ID 与订单号或请求 ID。