PAYATHON 2026

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。

备注:内容仅供参考。