概述
PayHub 支付平台为第三方商户提供统一收单 API。商户通过简单的 HTTP 接口即可快速接入多渠道支付能力。
| 支付方式 | 接口 | 适用场景 |
|---|---|---|
| 聚合扫码支付 | POST /api/cashier/qrcode-pay | PC / H5 展示二维码,用户扫码 |
| 微信 JSAPI | POST /api/cashier/wechat-js | 微信公众号 / 小程序内唤起支付 |
接入准备
获取凭证
在 PayHub 管理后台完成商户入驻后,获取以下信息:
| 凭证 | 说明 |
|---|---|
app_id | 商户应用标识(唯一) |
app_secret | 应用密钥,用于 HMAC-SHA256 签名 |
callback_url | 支付结果回调地址 |
⚠ 重要app_secret 仅创建时展示一次,请妥善保管。遗失可在管理后台重置。
环境地址
| 环境 | Base URL |
|---|---|
| 生产 | https://payment.qiquan18.cn |
| 测试 | https://payment.qiquan18.cn |
安全机制
| 机制 | 说明 |
|---|---|
| HMAC-SHA256 签名 | 每个请求必须携带签名,防篡改 |
| IP 白名单 | 商户可配置允许请求的服务器 IP 列表,非白名单 IP 直接返回 401 |
| 提单限频 | 同一 IP 3 秒内仅允许提交 1 次订单(基于 Redis),防止恶意刷单 |
| 时间戳校验 | timestamp 与服务器时间偏差超过 ±5 分钟则拒绝请求 |
| 随机串防重放 | nonce 随机字符串,防止请求被截获后重放 |
签名算法(HMAC-SHA256)
签名步骤
- 取所有请求参数(不含 sign),去掉值为空的参数
- 按 key 的 ASCII 升序排列
- 拼接成
key1=value1&key2=value2&...格式 - 以
app_secret为密钥,做 HMAC-SHA256 运算,输出 64 位小写 hex
签名示例
原始参数:
app_id = pk_live_51H8xK2eZvKYlo2C9xK2eZvKYlo2C
amount = 1000
merchant_order_no = ORD20260315001
notify_url = https://example.com/notify
timestamp = 1710489862
nonce = a1b2c3d4
排序拼接:
amount=1000&app_id=pk_live_51H8xK2eZvKYlo2C9xK2eZvKYlo2C&merchant_order_no=ORD20260315001&nonce=a1b2c3d4¬ify_url=https://example.com/notify×tamp=1710489862
签名:
sign = hmac_sha256(拼接字符串, app_secret) → "e5b7a3f1c9d2..."
多语言实现
// PHP
$params = [
'app_id' => $appId,
'amount' => 1000,
'merchant_order_no' => 'ORD20260315001',
'notify_url' => 'https://example.com/notify',
'timestamp' => time(),
'nonce' => bin2hex(random_bytes(16)),
];
// 1. 去空值 + 排序
$params = array_filter($params, fn($v) => $v !== '' && $v !== null);
ksort($params);
// 2. 拼接
$signStr = http_build_query($params); // 自动 key=value&key=value
// 3. HMAC-SHA256
$params['sign'] = hash_hmac('sha256', $signStr, $appSecret);
# Python
import hmac, hashlib, time, secrets, urllib.parse
params = {
'app_id': app_id,
'amount': 1000,
'merchant_order_no': 'ORD20260315001',
'notify_url': 'https://example.com/notify',
'timestamp': int(time.time()),
'nonce': secrets.token_hex(16),
}
# 去空值 + 排序 + 拼接
filtered = {k: v for k, v in params.items() if v not in ('', None)}
sign_str = '&'.join(f'{k}={v}' for k, v in sorted(filtered.items()))
# HMAC-SHA256
params['sign'] = hmac.new(
app_secret.encode(), sign_str.encode(), hashlib.sha256
).hexdigest()
// Java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.util.*;
import org.apache.commons.codec.binary.Hex;
Map<String, String> params = new TreeMap<>(); // TreeMap 自动排序
params.put("app_id", appId);
params.put("amount", "1000");
params.put("merchant_order_no", "ORD20260315001");
params.put("notify_url", "https://example.com/notify");
params.put("timestamp", String.valueOf(System.currentTimeMillis() / 1000));
params.put("nonce", UUID.randomUUID().toString().replace("-", ""));
// 拼接
StringBuilder sb = new StringBuilder();
params.forEach((k, v) -> {
if (v != null && !v.isEmpty()) {
if (sb.length() > 0) sb.append("&");
sb.append(k).append("=").append(v);
}
});
// HMAC-SHA256
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(appSecret.getBytes(), "HmacSHA256"));
String sign = Hex.encodeHexString(mac.doFinal(sb.toString().getBytes()));
// Node.js
const crypto = require('crypto');
const params = {
app_id: appId,
amount: 1000,
merchant_order_no: 'ORD20260315001',
notify_url: 'https://example.com/notify',
timestamp: Math.floor(Date.now() / 1000),
nonce: crypto.randomBytes(16).toString('hex'),
};
// 去空值 + 排序 + 拼接
const signStr = Object.keys(params)
.filter(k => params[k] !== '' && params[k] != null)
.sort()
.map(k => `${k}=${params[k]}`)
.join('&');
// HMAC-SHA256
params.sign = crypto.createHmac('sha256', appSecret)
.update(signStr).digest('hex');
// Go
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"sort"
"strings"
)
params := map[string]string{
"app_id": appId,
"amount": "1000",
"merchant_order_no": "ORD20260315001",
"notify_url": "https://example.com/notify",
"timestamp": fmt.Sprintf("%d", time.Now().Unix()),
"nonce": hex.EncodeToString(randomBytes(16)),
}
// 排序 + 拼接
keys := make([]string, 0, len(params))
for k, v := range params {
if v != "" { keys = append(keys, k) }
}
sort.Strings(keys)
pairs := make([]string, len(keys))
for i, k := range keys { pairs[i] = k + "=" + params[k] }
signStr := strings.Join(pairs, "&")
// HMAC-SHA256
h := hmac.New(sha256.New, []byte(appSecret))
h.Write([]byte(signStr))
sign := hex.EncodeToString(h.Sum(nil))
通用规范
请求格式
| 项目 | 要求 |
|---|---|
| 协议 | HTTPS(生产环境强制) |
| 方法 | POST |
| Content-Type | application/json |
| 字符编码 | UTF-8 |
公共请求参数
每个 API 请求都必须包含:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
app_id | string | ✅ | 商户应用 ID |
timestamp | int | ✅ | Unix 秒级时间戳,±5 分钟有效 |
nonce | string | ✅ | 随机字符串,≤32 位 |
sign | string | ✅ | HMAC-SHA256 签名 |
统一响应结构
{
"code": 200,
"message": "ok",
"data": { ... }
}
聚合扫码支付
生成聚合二维码,用户可使用 支付宝 / 微信 / 云闪付 扫码完成支付。
POST
/api/cashier/qrcode-pay
请求参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
app_id | string | ✅ | 商户应用 ID |
merchant_order_no | string | ✅ | 商户订单号,最长 64 位,回调原样返回 |
amount | int | ✅ | 支付金额,单位:分,最小值 1 |
notify_url | string | ✅ | 商户方回调地址,支付成功后通知此地址 |
body | string | ❌ | 商品描述 JSON,可为空 |
extra | string | ❌ | 自定义透传参数,回调原样返回 |
note | string | ❌ | 订单备注,最长 250 字符,默认 "在线支付" |
timestamp | int | ✅ | Unix 时间戳 |
nonce | string | ✅ | 随机字符串 |
sign | string | ✅ | HMAC-SHA256 签名 |
请求示例
POST /api/cashier/qrcode-pay HTTP/1.1
Content-Type: application/json
{
"app_id": "pk_live_51H8xK2eZvKYlo2C9xK2eZvKYlo2C",
"merchant_order_no": "ORD20260315001",
"amount": 1000,
"notify_url": "https://your-domain.com/pay/callback",
"body": "{\"goods\":\"VIP月卡\",\"sku\":\"vip_monthly\"}",
"extra": "{\"user_id\":12345,\"channel\":\"ios\"}",
"note": "VIP月卡充值",
"timestamp": 1710489862,
"nonce": "a1b2c3d4e5f6",
"sign": "e5b7a3f1c9d2e8b4a7f6..."
}
成功响应
{
"code": 200,
"message": "下单成功",
"data": {
"order_no": "QR20260315120000ABCD1234",
"merchant_order_no": "ORD20260315001",
"amount": 1000,
"extra": "{\"user_id\":12345,\"channel\":\"ios\"}",
"qr_code_url": "https://qr.example.com/xxxx.png",
"source_url": "alipays://platformapi/startapp?...",
"expire_minutes": 30
}
}
响应字段说明
| 字段 | 类型 | 说明 |
|---|---|---|
order_no | string | 平台订单号 |
merchant_order_no | string | 商户订单号(原样返回) |
amount | int | 金额(分) |
extra | string | 透传参数(原样返回) |
qr_code_url | string | 二维码图片 URL,前端直接 <img> 展示 |
source_url | string | 支付宝跳转直链(H5 场景可直跳唤起) |
expire_minutes | int | 二维码过期时间(分钟) |
💡 集成建议
PC 场景 — 将
H5 场景 — 可直接
qr_code_url 渲染为图片让用户扫码H5 场景 — 可直接
location.href = source_url 唤起支付宝错误响应示例
// 参数缺失 (422)
{"code":422,"message":"The merchant_order_no field is required.","data":{...}}
// 业务错误 (400)
{"code":400,"message":"金额必须大于 0","data":null}
// 签名错误 (401)
{"code":401,"message":"签名验证失败","data":null}
// 请求过频 (429)
{"code":429,"message":"请求过于频繁,请3秒后重试","data":null}
微信 JSAPI 支付 - 暂时未开通
用于微信公众号或小程序内唤起微信支付。
POST
/api/cashier/wechat-js
请求参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
app_id | string | ✅ | 商户应用 ID |
merchant_order_no | string | ✅ | 商户订单号 |
amount | int | ✅ | 金额(分) |
notify_url | string | ✅ | 回调地址 |
payeeMercId | string | ✅ | 收款方子商户号 |
subOpenId | string | ✅ | 用户在子商户公众号/小程序下的 OpenID |
wxAppId | string | ✅ | 微信公众号/小程序 AppID |
busiCode | string | ✅ | 业务代码 |
isMiniPg | string | ❌ | "1" 小程序、"2" 公众号(默认 "2") |
body | string | ❌ | 商品描述 JSON |
extra | string | ❌ | 透传参数 |
timestamp | int | ✅ | 时间戳 |
nonce | string | ✅ | 随机字符串 |
sign | string | ✅ | 签名 |
成功响应
{
"code": 200,
"message": "下单成功",
"data": {
"order_no": "JS20260315120000ABCD1234",
"merchant_order_no": "ORD20260315001",
"jsapi_pay_info": {
"appId": "wx1234567890abcdef",
"timeStamp": "1710489862",
"nonceStr": "a1b2c3d4e5f6g7",
"package": "prepay_id=wx15120000000000",
"signType": "RSA",
"paySign": "BASE64_SIGN_STRING..."
}
}
}
前端唤起支付
// 微信公众号 H5
WeixinJSBridge.invoke(
'getBrandWCPayRequest',
response.data.jsapi_pay_info,
function(res) {
if (res.err_msg === "get_brand_wcpay_request:ok") {
alert('支付成功');
}
}
);
// 微信小程序
wx.requestPayment({
...response.data.jsapi_pay_info,
success(res) { console.log('支付成功', res); },
fail(res) { console.log('支付失败', res); }
});
异步回调 (Webhook)
支付成功后,平台向商户的 notify_url 发送 POST 请求通知支付结果。
回调机制
| 项目 | 说明 |
|---|---|
| 触发条件 | 渠道确认支付成功 |
| 请求方式 | POST + application/json |
| 重试策略 | 商户未返回 "success" 时,1 分钟后重试,最多 3 次 |
| 重试间隔 | 1min → 1min → 1min(共 3 次推送) |
| 实现方式 | Laravel Queue Job + 延迟派发 |
| 幂等要求 | 商户须保证同一订单多次通知的幂等处理 |
回调参数
| 字段 | 类型 | 说明 |
|---|---|---|
order_no | string | 平台订单号 |
merchant_order_no | string | 商户订单号(原样返回) |
amount | string | 订单金额(元) |
actual_amount | string | 实际支付金额(元) |
status | int | 1 = 已支付 |
channel_code | string | 支付渠道编码 |
pay_time | string | 支付时间 YYYY-MM-DD HH:mm:ss |
extra | string | 透传参数(原样返回) |
sign | string | 回调签名(HMAC-SHA256) |
回调示例
POST https://your-domain.com/pay/callback HTTP/1.1
Content-Type: application/json
{
"order_no": "QR20260315120000ABCD1234",
"merchant_order_no": "ORD20260315001",
"amount": "10.00",
"actual_amount": "10.00",
"status": 1,
"channel_code": "YINSHENG_QRCODE",
"pay_time": "2026-03-15 12:30:00",
"extra": "{\"user_id\":12345,\"channel\":\"ios\"}",
"sign": "a1b2c3d4e5f6..."
}
商户验签 & 响应
收到回调后,用与请求签名相同的算法验证 sign,处理业务后返回:
HTTP 200
Content-Type: text/plain
success
⚠ 注意
必须返回纯文本
success(不是 JSON),否则平台将触发重试。订单状态枚举
| status | 含义 | 标识 |
|---|---|---|
| 0 | 待支付 | PENDING |
| 1 | 已支付 | PAID |
| 2 | 已关闭(超时) | CLOSED |
| 3 | 已退款 | REFUNDED |
| 4 | 支付失败 | FAILED |
支付渠道编码
| channel_code | 名称 | 类型 |
|---|---|---|
alipay_h5 | 支付宝 H5 | alipay |
alipay_pc | 支付宝 PC | alipay |
alipay_app | 支付宝 APP | alipay |
wxpay_h5 | 微信 H5 | wxpay |
wxpay_native | 微信 Native | wxpay |
wxpay_jsapi | 微信 JSAPI | wxpay |
wxpay_app | 微信 APP | wxpay |
unionpay_h5 | 银联 H5 | unionpay |
错误码
| HTTP Status | code | message 示例 | 处理建议 |
|---|---|---|---|
| 200 | 200 | 下单成功 | — |
| 400 | 400 | 商户订单号已存在,请勿重复提交 | 更换 merchant_order_no 后重试 |
| 400 | 400 | 金额必须大于 0 | 检查 amount 参数 |
| 401 | 401 | 缺少 app_id 参数 | 请求中加入 app_id |
| 401 | 401 | 商户不存在 / 商户已停用 | 确认 app_id 是否正确 |
| 401 | 401 | 请求已过期,请检查 timestamp | timestamp 与服务器时间差 ±5 分钟内 |
| 401 | 401 | IP 未授权 | 检查 IP 白名单配置 |
| 401 | 401 | 签名验证失败 | 检查签名算法与 app_secret |
| 422 | 422 | 商户订单号不能为空 | 补充必填参数 |
| 429 | 429 | 请求过于频繁,请3秒后重试 | 同一 app_id + IP 3 秒内限 1 次 |
| 500 | 500 | 服务异常 | 联系平台技术支持 |
接入流程
聚合扫码支付时序图
sequenceDiagram
participant M as 商户服务器
participant P as PayHub 平台
participant C as 支付渠道(银盛)
participant U as 用户
M->>P: 1. POST /api/cashier/qrcode-pay (带签名)
P->>P: 2. 验签 + IP白名单 + 订单防重 + 限频
P->>C: 3. 调用渠道下单
C-->>P: 4. 返回二维码URL
P-->>M: 5. 返回 qr_code_url + order_no
M->>U: 6. 展示二维码
U->>C: 7. 扫码支付
C->>P: 8. 渠道回调通知
P->>P: 9. 更新订单状态
P->>M: 10. Webhook (merchant_order_no + extra)
M-->>P: 11. 返回 "success"
步骤说明
- 商户服务器 → PayHub:POST /api/cashier/qrcode-pay(携带签名)
- PayHub:验签 → 商户状态 → 时间戳 → IP 白名单 → 订单防重 → 限频检查
- PayHub → 支付渠道:调用渠道下单接口
- 支付渠道 → PayHub:返回二维码 URL
- PayHub → 商户服务器:返回 qr_code_url + order_no
- 商户前端 → 用户:展示二维码
- 用户:扫码完成支付
- 支付渠道 → PayHub:异步回调通知
- PayHub:更新订单状态
- PayHub → 商户 notify_url:Webhook 推送(含 merchant_order_no + extra)
- 商户服务器 → PayHub:返回 "success"
SDK & 示例代码
以下为各语言的核心调用代码,包含签名计算与下单请求。
// PHP — 签名 + 下单
$params = [
'app_id' => $appId,
'merchant_order_no' => 'ORD' . date('YmdHis') . mt_rand(1000, 9999),
'amount' => 1000,
'notify_url' => 'https://your-domain.com/pay/callback',
'extra' => json_encode(['user_id' => 12345]),
'timestamp' => time(),
'nonce' => bin2hex(random_bytes(16)),
];
// 签名
$filtered = array_filter($params, fn($v) => $v !== '' && $v !== null);
ksort($filtered);
$signStr = urldecode(http_build_query($filtered));
$params['sign'] = hash_hmac('sha256', $signStr, $appSecret);
// 下单
$ch = curl_init($baseUrl . '/api/cashier/qrcode-pay');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode($params),
CURLOPT_RETURNTRANSFER => true,
]);
$result = json_decode(curl_exec($ch), true);
// $result['data']['qr_code_url'] → 二维码图片
// Java — 签名 + 下单 (需引入 okhttp、commons-codec)
TreeMap<String, String> params = new TreeMap<>();
params.put("app_id", appId);
params.put("merchant_order_no", "ORD" + System.currentTimeMillis());
params.put("amount", "1000");
params.put("notify_url", "https://your-domain.com/pay/callback");
params.put("extra", "{\"user_id\":12345}");
params.put("timestamp", String.valueOf(System.currentTimeMillis() / 1000));
params.put("nonce", UUID.randomUUID().toString().replace("-", ""));
// 签名
StringBuilder sb = new StringBuilder();
params.forEach((k, v) -> {
if (v != null && !v.isEmpty()) {
if (sb.length() > 0) sb.append("&");
sb.append(k).append("=").append(v);
}
});
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(appSecret.getBytes(), "HmacSHA256"));
String sign = Hex.encodeHexString(mac.doFinal(sb.toString().getBytes()));
params.put("sign", sign);
// 下单
RequestBody body = RequestBody.create(
new Gson().toJson(params), MediaType.parse("application/json"));
Request request = new Request.Builder()
.url(baseUrl + "/api/cashier/qrcode-pay")
.post(body).build();
Response response = new OkHttpClient().newCall(request).execute();
// 解析 response.body().string()
# Python — 签名 + 下单
import hmac, hashlib, time, secrets, requests, json
params = {
'app_id': app_id,
'merchant_order_no': f'ORD{int(time.time())}',
'amount': 1000,
'notify_url': 'https://your-domain.com/pay/callback',
'extra': json.dumps({'user_id': 12345}),
'timestamp': int(time.time()),
'nonce': secrets.token_hex(16),
}
# 签名
filtered = {k: v for k, v in params.items() if v not in ('', None)}
sign_str = '&'.join(f'{k}={v}' for k, v in sorted(filtered.items()))
params['sign'] = hmac.new(
app_secret.encode(), sign_str.encode(), hashlib.sha256
).hexdigest()
# 下单
resp = requests.post(
f'{base_url}/api/cashier/qrcode-pay', json=params
).json()
# resp['data']['qr_code_url'] → 二维码图片
// Node.js — 签名 + 下单
const crypto = require('crypto');
const axios = require('axios');
const params = {
app_id: appId,
merchant_order_no: `ORD${Date.now()}`,
amount: 1000,
notify_url: 'https://your-domain.com/pay/callback',
extra: JSON.stringify({ user_id: 12345 }),
timestamp: Math.floor(Date.now() / 1000),
nonce: crypto.randomBytes(16).toString('hex'),
};
// 签名
const signStr = Object.keys(params)
.filter(k => params[k] !== '' && params[k] != null)
.sort()
.map(k => `${k}=${params[k]}`)
.join('&');
params.sign = crypto.createHmac('sha256', appSecret)
.update(signStr).digest('hex');
// 下单
const { data } = await axios.post(
`${baseUrl}/api/cashier/qrcode-pay`, params
);
// data.data.qr_code_url → 二维码图片
// Go — 签名 + 下单
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"sort"
"strings"
"time"
)
params := map[string]string{
"app_id": appId,
"merchant_order_no": fmt.Sprintf("ORD%d", time.Now().Unix()),
"amount": "1000",
"notify_url": "https://your-domain.com/pay/callback",
"extra": `{"user_id":12345}`,
"timestamp": fmt.Sprintf("%d", time.Now().Unix()),
"nonce": hex.EncodeToString(randomBytes(16)),
}
// 签名
keys := make([]string, 0)
for k, v := range params {
if v != "" { keys = append(keys, k) }
}
sort.Strings(keys)
pairs := make([]string, len(keys))
for i, k := range keys { pairs[i] = k + "=" + params[k] }
signStr := strings.Join(pairs, "&")
h := hmac.New(sha256.New, []byte(appSecret))
h.Write([]byte(signStr))
params["sign"] = hex.EncodeToString(h.Sum(nil))
// 下单
jsonData, _ := json.Marshal(params)
resp, _ := http.Post(
baseUrl+"/api/cashier/qrcode-pay",
"application/json",
bytes.NewBuffer(jsonData),
)
// 解析 resp.Body → data.qr_code_url
<?php
/**
* 聚合扫码支付 API 完整调用示例
*
* 包含签名计算、下单请求、响应处理的完整流程
*/
$appId = 'your_app_id'; // 公钥
$appSecret = 'your_app_secret'; // 秘钥
$baseUrl = 'https://payment.qiquan18.cn/api/cashier'; // 生产环境地址
// ─── 正确签名下单 ───
$orderNo = 'ORD_' . date('YmdHis') . '_' . mt_rand(1000, 9999);
$params = buildParams($appId, 1000, $orderNo); // 1000分 = 10元
$params['sign'] = makeSign($params, $appSecret);
echo "商户订单号: {$orderNo}\n";
$res = httpPost($baseUrl . '/qrcode-pay', $params);
echo "响应: " . json_encode($res['json'], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n";
if ($res['json']['code'] === 200) {
$qrCodeUrl = $res['json']['data']['qr_code_url'];
echo "二维码: {$qrCodeUrl}\n";
}
// ────── 工具函数 ──────
function buildParams(string $appId, int $amount, string $merchantOrderNo): array
{
return [
'app_id' => $appId,
'merchant_order_no' => $merchantOrderNo,
'amount' => $amount,
'notify_url' => 'https://your-domain.com/pay/callback',
'extra' => json_encode(['user_id' => 12345]),
'note' => '在线支付',
'timestamp' => time(),
'nonce' => bin2hex(random_bytes(16)),
];
}
function makeSign(array $params, string $secret): string
{
unset($params['sign']);
$params = array_filter($params, fn($v) => $v !== '' && $v !== null);
ksort($params);
$pairs = [];
foreach ($params as $k => $v) {
$pairs[] = "{$k}={$v}";
}
$signStr = implode('&', $pairs);
return hash_hmac('sha256', $signStr, $secret);
}
function httpPost(string $url, array $data): array
{
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Accept: application/json'],
CURLOPT_POSTFIELDS => json_encode($data),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
]);
$body = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
return [
'http_code' => $httpCode,
'body' => $body,
'error' => $error,
'json' => json_decode($body, true),
];
}