PHP 集成 QuickBooks Payment 时出现 403 错误
结论
创建 Invoice 需要调用 QuickBooks Online Accounting API,因此 access token 必须包含以下 scope:
com.intuit.quickbooks.accounting
如果还要调用 QuickBooks Payments API,可以同时申请:
com.intuit.quickbooks.accounting com.intuit.quickbooks.payment
当前 token 很可能只申请了 Payments scope,或者没有包含 Accounting scope。修改 PHP 中的 scope 配置不会改变已有 token 的权限。必须让用户重新完成 OAuth2 授权,取得新的 authorization code、access token 和 refresh token。
为什么会返回 403
以下错误:
AuthorizationFailed
errorCode=003100
statusCode=403
The access token does not have sufficient scope
表示请求携带了有效的 OAuth2 token,但该 token 无权访问当前 API。
Invoice 属于 QuickBooks Online 的会计数据,不属于 Payments API。即使项目主要集成 QuickBooks Payment,只要代码通过:
$dataService->Add($theResourceObj);
创建 Invoice,就必须申请 com.intuit.quickbooks.accounting。
当前错误与 Invoice 数据结构、CustomerRef 或 ItemRef 暂时无关。如果这些数据有误,通常会收到参数错误或业务验证错误,而不是 "insufficient scope"。
重新配置 OAuth2 scope
初始化用于发起授权的 DataService 时,需要明确配置所需的 scope:
<?php
require "../vendor/autoload.php";
use QuickBooksOnline\API\DataService\DataService;
$dataService = DataService::Configure([
'auth_mode' => 'oauth2',
'ClientID' => 'YOUR_CLIENT_ID',
'ClientSecret' => 'YOUR_CLIENT_SECRET',
'RedirectURI' => 'https://example.com/quickbooks/callback.php',
// 只创建 Invoice 时使用 Accounting scope
'scope' => 'com.intuit.quickbooks.accounting',
'baseUrl' => 'Development',
]);
$oauth2LoginHelper = $dataService->getOAuth2LoginHelper();
$authorizationUrl = $oauth2LoginHelper->getAuthorizationCodeURL();
header('Location: ' . $authorizationUrl);
exit;
如果同一个连接既要操作 Invoice,也要调用 Payments API,则同时配置两个 scope:
'scope' => 'com.intuit.quickbooks.accounting com.intuit.quickbooks.payment',
多个 scope 之间用空格分隔,不要使用逗号。
在回调地址中换取新 token
用户完成 Intuit 授权后,Intuit 会将 code 和 realmId 返回到配置的回调地址。回调处理可以写成:
<?php
require "../vendor/autoload.php";
use QuickBooksOnline\API\DataService\DataService;
$dataService = DataService::Configure([
'auth_mode' => 'oauth2',
'ClientID' => 'YOUR_CLIENT_ID',
'ClientSecret' => 'YOUR_CLIENT_SECRET',
'RedirectURI' => 'https://example.com/quickbooks/callback.php',
'scope' => 'com.intuit.quickbooks.accounting',
'baseUrl' => 'Development',
]);
$authorizationCode = $_GET['code'] ?? null;
$realmId = $_GET['realmId'] ?? null;
if (!$authorizationCode || !$realmId) {
throw new RuntimeException('OAuth2 callback is missing code or realmId.');
}
$oauth2LoginHelper = $dataService->getOAuth2LoginHelper();
$accessToken = $oauth2LoginHelper->exchangeAuthorizationCodeForToken(
$authorizationCode,
$realmId
);
$dataService->updateOAuth2Token($accessToken);
// 应保存到数据库或其他安全存储中
$newAccessToken = $accessToken->getAccessToken();
$newRefreshToken = $accessToken->getRefreshToken();
echo 'QuickBooks authorization completed.';
实际项目中,需要关联保存以下三项:
access token
refresh token
realmId
realmId 是已授权的 QuickBooks company ID,后续需要将它配置为 QBORealmID。
不要把 token 输出到页面、日志或版本库中。
使用新 token 创建 Invoice
重新授权后,使用新取得的 token 初始化 DataService:
<?php
require "../vendor/autoload.php";
use QuickBooksOnline\API\DataService\DataService;
use QuickBooksOnline\API\Facades\Invoice;
use QuickBooksOnline\API\Exception\ServiceException;
$dataService = DataService::Configure([
'auth_mode' => 'oauth2',
'ClientID' => 'YOUR_CLIENT_ID',
'ClientSecret' => 'YOUR_CLIENT_SECRET',
'accessTokenKey' => 'NEW_ACCESS_TOKEN',
'refreshTokenKey' => 'NEW_REFRESH_TOKEN',
'QBORealmID' => 'AUTHORIZED_REALM_ID',
'baseUrl' => 'Development',
]);
$dataService->throwExceptionOnError(true);
$invoice = Invoice::create([
'Line' => [
[
'Amount' => 100.00,
'DetailType' => 'SalesItemLineDetail',
'SalesItemLineDetail' => [
'ItemRef' => [
'value' => '20',
],
],
],
],
'CustomerRef' => [
'value' => '1',
],
'BillEmail' => [
'Address' => 'Familiystore@intuit.com',
],
]);
try {
$result = $dataService->Add($invoice);
echo 'Created Invoice ID: ' . $result->Id;
} catch (ServiceException $e) {
echo 'QuickBooks request failed: ' . $e->getMessage();
}
代码启用了:
$dataService->throwExceptionOnError(true);
因此,SDK 遇到错误时会直接抛出异常,需要使用 try...catch 处理。在这种模式下,异常抛出后,原代码中的 $dataService->getLastError() 通常无法执行。
Refresh token 不能补充 scope
刷新现有 token 无法增加 Accounting 权限。refresh token 只能在原授权范围内获取新的 access token,例如:
$oauth2LoginHelper = $dataService->getOAuth2LoginHelper();
$newAccessToken = $oauth2LoginHelper->refreshAccessTokenWithRefreshToken(
$storedRefreshToken
);
如果原 token 只包含:
com.intuit.quickbooks.payment
刷新后仍然不会获得:
com.intuit.quickbooks.accounting
要增加该权限,必须修改授权请求中的 scope,并让用户重新授权。
刷新成功后,还要保存返回的最新 access token 和 refresh token,不能假定旧 refresh token 可以长期使用。
还需要检查的配置
如果重新授权后仍然失败,可以逐项确认:
- Intuit Developer 应用已启用 QuickBooks Online Accounting API 所需能力。
- 授权请求包含
com.intuit.quickbooks.accounting。 RedirectURI与 Intuit Developer 控制台登记的地址完全一致。Development环境使用 sandbox company 的realmId和 token。- Production 环境使用生产授权生成的 token,并将
baseUrl设置为Production。 ClientID、ClientSecret、token 和realmId来自同一个 Intuit 应用,并对应同一次正确的环境授权。- 数据库中的旧 token 已被替换,程序没有继续读取此前仅包含 Payments scope 的 token。
CustomerRef.value对应当前 company 中存在的 Customer。ItemRef.value对应当前 company 中存在且可用于销售的 Item。
最后两项不能解决当前的 scope 403,但会影响权限问题解决后的 Invoice 创建请求。
处理顺序如下:申请 com.intuit.quickbooks.accounting → 让用户重新授权 → 使用 authorization code 换取新 token → 保存新 token 和 realmId → 再调用 Invoice API。
备注:内容仅供参考。
