PAYATHON 2026

如何为 Xero Payment API 设置分页?

支付阿杰

结论

GET Payments 通常只支持通过 page 参数翻页,不能指定每页返回多少条数据。请求需要从第 1 页开始,逐页获取,直到接口返回空数组或不足一页的数据。

Payment 本身不包含完整的 LineItems,通常只会提供关联发票等对象的引用。要获取明细行,可以根据 Payment 中的 InvoiceID 调用 GET Invoices/{InvoiceID};也可以先分页同步发票,再通过 InvoiceID 关联两类数据。

分页获取全部 Payments

Accounting API 的请求形式如下:

GET https://api.xero.com/api.xro/2.0/Payments?page=1
GET https://api.xero.com/api.xro/2.0/Payments?page=2
GET https://api.xero.com/api.xro/2.0/Payments?page=3

由于不能设置每页数量,不要依赖自定义 pageSize。可以持续请求下一页,直到:

  • Payments 为空;或
  • 返回数量少于接口固定的单页上限。

固定上限可能发生变化,因此以返回空页作为最终停止条件更稳妥。

async function getAllPayments(accessToken, tenantId) {
  const payments = [];

  for (let page = 1; ; page += 1) {
    const response = await fetch(
      `https://api.xero.com/api.xro/2.0/Payments?page=${page}`,
      {
        headers: {
          Authorization: `Bearer ${accessToken}`,
          "Xero-tenant-id": tenantId,
          Accept: "application/json"
        }
      }
    );

    if (!response.ok) {
      throw new Error(
        `GET Payments failed: ${response.status} ${await response.text()}`
      );
    }

    const data = await response.json();
    const currentPage = data.Payments ?? [];

    if (currentPage.length === 0) {
      break;
    }

    payments.push(...currentPage);
  }

  return payments;
}

获取关联发票的 LineItems

Payment 返回的 Invoice 通常只是关联对象的摘要,不能假定其中包含完整的 LineItems。可以提取并去重 InvoiceID,再逐一读取发票详情:

async function getInvoice(invoiceId, accessToken, tenantId) {
  const response = await fetch(
    `https://api.xero.com/api.xro/2.0/Invoices/${encodeURIComponent(invoiceId)}`,
    {
      headers: {
        Authorization: `Bearer ${accessToken}`,
        "Xero-tenant-id": tenantId,
        Accept: "application/json"
      }
    }
  );

  if (!response.ok) {
    throw new Error(
      `GET Invoice failed: ${response.status} ${await response.text()}`
    );
  }

  const data = await response.json();
  return data.Invoices?.[0] ?? null;
}

async function getPaymentsWithInvoiceLines(accessToken, tenantId) {
  const payments = await getAllPayments(accessToken, tenantId);

  const invoiceIds = [
    ...new Set(
      payments
        .map(payment => payment.Invoice?.InvoiceID)
        .filter(Boolean)
    )
  ];

  const invoicesById = new Map();

  // 使用串行请求便于控制速率;生产环境也可以采用有限并发。
  for (const invoiceId of invoiceIds) {
    const invoice = await getInvoice(invoiceId, accessToken, tenantId);

    if (invoice) {
      invoicesById.set(invoiceId, invoice);
    }
  }

  return payments.map(payment => {
    const invoiceId = payment.Invoice?.InvoiceID;

    return {
      ...payment,
      InvoiceDetails: invoiceId
        ? invoicesById.get(invoiceId) ?? null
        : null
    };
  });
}

发票行可以从以下位置读取:

result[0].InvoiceDetails?.LineItems

数据量较大时的做法

如果逐笔读取发票,API 请求数量可能会很大。数据量较多时,可以采用下面的同步流程:

  1. 分页拉取 Payments。
  2. 分页拉取 Invoices,并保存完整的发票数据。
  3. 通过 InvoiceID 在本地关联两类数据。
  4. 后续使用 If-Modified-Since 等增量机制更新,避免每次全量读取。

筛选参数、分页大小参数和增量请求头是否可用,应以当前使用的 Xero Accounting API 版本及 SDK 文档为准。其他 endpoint 支持的参数不一定适用于 GET Payments。

注意事项

  • Payment 可能关联 Invoice、Credit Note、Prepayment 或 Overpayment,不能假定每条记录都有 Payment.Invoice.InvoiceID。
  • 同一张发票可能对应多笔 Payment,因此要先对 InvoiceID 去重。
  • 需要处理 401、429 和 5xx 响应。遇到限流时,应按照响应头指定的等待时间重试。
  • 分页过程中,数据可能发生变化。需要稳定同步时,可以配合时间范围或增量条件,并按 PaymentID 去重。
  • 不要根据 Payment 的总金额推导各个 LineItem 的实际付款分摊。Payment 通常针对整张发票,支付金额与发票明细行之间未必存在一一对应关系。

备注:内容仅供参考。