PAYATHON 2026

Xero Contacts API 为何只更新 Bills 而忽略 Sales?

支付阿杰

结论

这份 PaymentTerms payload 结构没有问题,也不需要增加 OAuth scope、请求头或 SalesDefaultAccountCode。

首先要确认目标联系人在 Xero 中是否已经是客户,也就是返回数据中是否包含:

"IsCustomer": true

Xero 会根据联系人实际参与的业务区分客户和供应商:

  • Bills 是供应商的采购付款条件;
  • Sales 是客户的销售付款条件。

如果联系人只有 IsSupplier: true,而 IsCustomer: false,Xero 可能仍会接受整个请求并返回 HTTP 200,但只应用 Bills,忽略与当前联系人身份不符的 Sales。HTTP 200 只表示 Xero 已处理请求,不代表所有嵌套字段都已更新。

先确认联系人身份

更新前,先查询目标联系人:

GET /api.xro/2.0/Contacts/{ContactID}
xero-tenant-id: <tenant-id>
Authorization: Bearer <token>
Accept: application/json

检查响应中的以下字段:

{
  "Contacts": [
    {
      "ContactID": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "IsCustomer": false,
      "IsSupplier": true,
      "PaymentTerms": {
        "Bills": {
          "Day": 7,
          "Type": "DAYSAFTERBILLDATE"
        },
        "Sales": {
          "Day": 30,
          "Type": "DAYSAFTERBILLDATE"
        }
      }
    }
  ]
}

如果 IsCustomer 是 false,通常可以据此解释为什么 Sales 没有更新。

不能通过 payload 强制设置客户身份

不要在联系人更新请求中加入:

"IsCustomer": true,
"IsSupplier": true

这两个字段用于反映联系人是否参与过应收或应付业务,通常由 Xero 根据交易记录维护,并不是可靠的可写开关。即使传入这些字段,Xero 也可能忽略它们。

联系人需要通过真实业务成为 Xero 中的客户,例如已有以该联系人为对象的销售交易。不要为了改变身份标志而创建虚假发票,应按照实际业务流程建立应收关系。

当联系人同时具有以下状态时:

"IsCustomer": true,
"IsSupplier": true

原来的请求结构就可以同时更新两组付款条件:

{
  "Contacts": [
    {
      "ContactID": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "PaymentTerms": {
        "Bills": {
          "Day": 7,
          "Type": "DAYSAFTERBILLDATE"
        },
        "Sales": {
          "Day": 7,
          "Type": "DAYSAFTERBILLDATE"
        }
      }
    }
  ]
}

Apex 调用可以继续使用现有结构

现有调用结构可以保留,但应补充参数检查,并在调用后核对响应中的联系人数据,不能只记录 HTTP 状态码:

@future(callout=true)
public static void updatePaymentTerms(
    String contactId,
    String term,
    String tenantId,
    String namedCredentialName
) {
    if (
        String.isBlank(contactId) ||
        String.isBlank(tenantId) ||
        String.isBlank(namedCredentialName)
    ) {
        return;
    }

    XeroPaymentTermMapper.XeroPaymentTerm terms =
        XeroPaymentTermMapper.mapPaymentTerm(term);

    if (terms == null) {
        return;
    }

    Map<String, Object> paymentTerm = new Map<String, Object>{
        'Day' => terms.Day,
        'Type' => terms.Type
    };

    Map<String, Object> payload = new Map<String, Object>{
        'Contacts' => new List<Object>{
            new Map<String, Object>{
                'ContactID' => contactId,
                'PaymentTerms' => new Map<String, Object>{
                    'Bills' => new Map<String, Object>(paymentTerm),
                    'Sales' => new Map<String, Object>(paymentTerm)
                }
            }
        }
    };

    HttpRequest req = new HttpRequest();
    req.setEndpoint(
        'callout:' + namedCredentialName + '/api.xro/2.0/Contacts'
    );
    req.setMethod('POST');
    req.setHeader('xero-tenant-id', tenantId);
    req.setHeader('Accept', 'application/json');
    req.setHeader('Content-Type', 'application/json');
    req.setBody(JSON.serialize(payload));

    HttpResponse res = new Http().send(req);

    System.debug(LoggingLevel.INFO, 'Xero status: ' + res.getStatusCode());
    System.debug(LoggingLevel.INFO, 'Xero response: ' + res.getBody());

    if (res.getStatusCode() < 200 || res.getStatusCode() >= 300) {
        throw new CalloutException(
            'Xero contact update failed: ' +
            res.getStatusCode() + ' ' + res.getBody()
        );
    }
}

更新完成后,再次执行 GET /Contacts/{ContactID},确认 Xero 实际保存的 PaymentTerms.Sales。不要只根据 Salesforce 发出的 JSON 或 HTTP 200 判断更新结果。

如果 IsCustomer 已经是 true

如果查询结果明确显示:

"IsCustomer": true

但 Sales 仍未变化,联系人身份就不是原因。可以依次检查:

  1. POST 响应中返回的 PaymentTerms.Sales 是否已经是新值。
  2. 更新与查询使用的 xero-tenant-id 是否完全一致。
  3. ContactID 是否指向预期联系人,而不是联系人编号或其他 Salesforce 字段。
  4. terms.Day 是否为整数,terms.Type 是否为 Xero 接受的枚举值,例如 DAYSAFTERBILLDATE。
  5. 请求完成后,是否有其他同步任务、Xero App 或 Salesforce Flow 覆盖了该字段。
  6. 尝试只发送 Sales,检查问题是否与组合更新有关:
{
  "Contacts": [
    {
      "ContactID": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "PaymentTerms": {
        "Sales": {
          "Day": 7,
          "Type": "DAYSAFTERBILLDATE"
        }
      }
    }
  ]
}

如果联系人已经是客户,单独更新 Sales 仍被静默忽略,响应中也没有 ValidationErrors,问题更可能与特定租户或 Xero 服务端行为有关。此时应保存完整响应、请求时间、tenant ID、ContactID,以及响应头中的请求或关联标识,然后提交给 Xero 支持核查。继续向 payload 添加未定义字段无法帮助定位问题。

备注:内容仅供参考。