开发者/功能接入

商品与下单

本篇按采购流程依次说明:查余额、找商品、看价格、下单,最后获取卡密。

相关文档:认证与签名 · 通用约定 · Webhook 通知

#账户与余额

#账户信息

GET/api/v1/account 返回你的企业信息,以及 API 是否已开通。

JSON
{
  "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 返回你当前能用多少钱。

JSON
{
  "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 从你的钱包扣款,这个汇率只用于你自己定价。

JSON
{
  "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修正请求
404No exchange rate for this currency.这个币种请自行定价
503Exchange 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。示例:

HTTP
GET /api/v1/skus?search=steam&region=US&limit=50&offset=0

筛选条件(都是可选的):

参数示例匹配内容
searchsteamSKU 编号、名称或品牌
brandSteam品牌名(不区分大小写)
regionUS国家代码或国家名称;GLC 表示全球,EU 表示欧洲
categoryTravel业务分类,例如 Travel、Digital Wallets & Payment、Game Credits
verticalgift_card商品线
product_typepin_code交付方式

分页:传 limit(默认 100,最大 500)和 offset。不断增大 offset 继续请求,直到 offset 达到 count。

JSON
{
  "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 询价。
availabilityavailable(可售)或 unavailable(不可售)。只能购买可售的 SKU。
denomination_typefixed(固定面值)或 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_labelmerchant_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_typepin_code(交付卡密)或 direct_charge(直接充值到账户)。
delivery_type供应商交付的形式:code、code_pin、code_serial、url 或 direct_charge。delivery_mode 为 voucher 时,你收到的是链接。
delivery_modecode(直接交付卡密)或 voucher(交付 CardV 兑换链接)。
voucher_instructions仅 voucher SKU:告诉客户如何使用该链接的说明文字。其他情况为 ""。
vertical、category产品线和业务分类,用于筛选和展示。
region、region_name、region_label国家或地区代码、名称和展示文本。
required_input_schema直充商品需要你提供的信息。
brand_logo_url、image_urlCardV 托管的图片地址,没有时为 ""。
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。

HTTP
GET /api/v1/products?brand=Steam&region=GB&limit=50&offset=0

筛选和分页参数与 GET/api/v1/skus 相同。count 是商品数,sku_count 是这些商品下的 SKU 总数。

JSON
{
  "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 会列出需要哪些字段:

JSON
"required_input_schema": [
  {"key": "player_id", "label": "Player ID", "type": "text", "required": true},
  {"key": "server", "label": "Server", "type": "text", "required": false}
]

下单时,在订单行的 inputs 里按 key 填写对应的值:

JSON
"inputs": {"player_id": "123456789", "server": "EU"}
  • 除非标明 "required": false,否则都是必填字段。type 是给你的表单字段的类型提示,例如 text。
  • 缺少必填值时,订单会被拒绝,并返回 items 错误。
  • 这些是你客户的个人信息,请妥善保护(见安全)。

#报价

报价用于查询某个数量下的当前价格。

HTTP
GET /api/v1/skus/S000456/quote?quantity=2
GET /api/v1/skus/S000789/quote?quantity=1&amount=25.00
JSON
{
  "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,并从钱包扣款。和所有请求一样,这个请求必须签名。

JSON
{
  "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:

JSON
{
  "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这个商户订单号已经用于另一笔不同的订单见安全重试

价格变化时的错误示例:

JSON
{
  "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,它必须和第一次下单时的价格一致。

系统会先识别是否为重复提交,再检查余额和价格。所以即使价格在这期间变了,重复提交也总是返回原来那笔订单。

#重试流程

没有收到明确结果时,直接把同一笔订单再提交一次即可。

Text
用商户订单号 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} 返回订单详情、状态和卡密。

JSON
{
  "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。

#订单状态与卡密

#订单状态

Text
accepted ──► processing ──► succeeded
                  │
                  ├──► partially_succeeded   (部分到货,部分未到货)
                  │
                  └──► failed ──► refunded   (退款回到钱包)
状态是否结束怎么处理
accepted(已接受)否等待。已扣款,还没开始发货。
processing(处理中)否等待。不要重新下单。
succeeded(成功)是读取卡密,交付给客户。
partially_succeeded(部分成功)是交付已到货的部分,其余部分稍后退款。
failed(失败)尚未等待变为 refunded。失败不等于已退款。
refunded(已退款)是钱已经退回你的钱包。

如果不用 Webhook,建议这样轮询:5 秒后查一次,然后 10 秒、30 秒、60 秒,之后每 5 分钟一次,并注意不要超过频率限制。大多数订单几秒内就能完成,个别需要人工处理的可能要几个小时。

#卡密

每个已交付的单位是 items[].deliveries 里的一个对象:

JSON
{
  "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。