Stripe的付款API:前十年

2020-12-16 14:02:53

几年前,《彭博商业周刊》在Stripe上发表了一篇专题报道。封面的中心有四个字:“七行代码”,表明这是一家企业在Stripe上付款的全部方法。断言是大胆的,并成为我们的主题和模因。

到目前为止,尚不清楚本文引用了哪七行。普遍的理论是,创建电荷需要大约七行卷曲。 2011年,目标网页上的代码段长了9行。但是删除可选的说明和卡片[cvc],从视觉上看,它有七行:

但是,搜索七行代码最终会失去重点:打开终端,运行此卷曲代码段,然后立即看到成功进行信用卡付款的感觉就像是七行代码。开发人员不太可能相信生产就绪型付款集成实际上仅涉及七行代码。但是,采用像信用卡处理这样复杂的方法并将集成减少到只有几行代码,这些代码在运行时立即返回成功的Charge对象确实是很神奇的。

在过去十年中,抽象出支付的复杂性已推动了我们API的发展。这篇文章提供了API设计背后的上下文,拐点和概念框架。我们API的方法成为商业杂志的封面,这是一个极端的例外。这篇文章分享了我们在这七个方面之外的发展情况。

成功的产品会随着时间的推移有机地扩展,从而导致产品欠债。与技术债务类似,产品债务会逐渐累积,使产品难以为用户所理解,也难以为产品团队带来变化。对于API产品,特别容易产生产品债务,因为很难使用户从根本上重组他们的集成;让他们在现有的API请求中添加一个或两个参数要容易得多。

回顾过去,我们可以清楚地看到我们的API是如何发展的,以及哪些决定对塑造它们至关重要。这是定义我们的支付API并导致创建PaymentIntents API的里程碑。

我们首先在美国推出了Stripe API,在这里,信用卡是(现在仍然是)主要的付款方式。 “七行代码”已经足够了,但是现实只稍微复杂了一点。我们还创建了Stripe.js,这是一个JavaScript库,用于从浏览器中收集信用卡付款详细信息,并将其安全地存储在Stripe中,并以Token的形式存储,随后可用于创建费用。这帮助用户避免了繁琐的PCI合规性要求。

这种支付流程遵循传统Web应用程序中非常普遍的模式。 JavaScript客户端使用可发布的API密钥来创建令牌,并在客户提交付款表单(以及有关订单的其他表单数据)时将两者发送到服务器。服务器使用该令牌和API密钥同步创建费用;可以根据付款结果选择完成订单。

当我们首次创建费用和令牌时,它们仅支持信用卡付款。随着我们扩展到更多国家和用户类型,我们需要向API添加更多付款方式。在2015年,我们添加了:

ACH借记,这是自1970年代以来在美国常见的付款方式。在美国银行帐户之间转移资金时使用ACH,它支持银行帐户的贷记和借记。

比特币,在2010年代初期刚刚获得关注。越来越多的企业正在尝试接受比特币作为支付方式。

当用户有足够的信心保证资金时,我们将付款描述为“最终”。 (当然,由于欺诈或随后的退款,即使是最终付款也可以在以后撤回。)在大多数情况下,一旦完成,用户就会放行货物。虽然在卡网络上处理的付款是由商家发起并可以立即完成的,但这两种付款方式与卡完全不同。在ACH网络上处理的付款将在几天后完成。使用比特币,客户(而非商人)确定何时创建比特币交易。像ACH付款一样,比特币付款也不会立即完成。尽管商家会知道,一旦区块将其提起,客户便已创建了比特币交易,但它仍需要6个区块(约一个小时)才能完成交易。

前三种付款方式中的每一种在付款方式和保证资金的时间方面都不同。这使得创建将它们之间的差异抽象化的API的任务非常具有挑战性。

ACH借记卡。由于卡付款和ACH借记付款都只需要来自客户的静态信息(即卡号或银行帐号),因此我们扩展了令牌资源,以代表卡详细信息和银行帐户详细信息。用户仍然可以通过任何一种令牌创建费用,但是我们在费用中添加了待处理状态,以表示ACH借记费用尚未立即确定,并且仍然可能失败。几天后,当用户收到一个表示收费成功的网络挂钩时,​​用户便运行了其订单履行逻辑。

比特币。由于比特币不适合我们的抽象概念,因此我们不得不引入新的BitcoinReceiver API,以方便我们需要客户采取付款流程的客户端操作。特别对于Stripe而言,“接收者”是临时存放资金的地方。它有一个非常简单的状态机来描述接收方的状态:一个布尔值,填充的,是对还是错。接收者装满后,用户可以使用该BitcoinReceiver对象而不是Token对象创建一个Charge。这实际上会将资金从接收者转移到用户的余额。如果用户未在特定时间内创建费用,收款人的款项将退还给客户。像ACH借记费用一样,比特币费用从待定状态开始,然后异步成功。

有了ACH借记卡和比特币,整合变得更加复杂。现在,它涉及到异步付款完成处理,在比特币的情况下,它涉及管理两个状态机以完成付款:客户端上的BitcoinReceiver和服务器上的Charge。

在接下来的两年中,我们增加了更多的付款方式。他们中的大多数人都比比特币更像比特币,而不是卡,他们需要客户采取行动才能开始付款。我们发现,为每种资源引入全新的类似BitcoinReceiver的资源对开发人员都不友好,只会引入太多新的Stripe特定概念以在API中进行推理。我们希望设计一个更简单的支付API,并开始探索如何在一种集成路径上统一这些支付方法:Sources API。

我们将先前设计的两个客户端抽象(令牌和BitcoinReceivers)组合到了一个客户端驱动的状态机中,该状态机称为Source。创建后,来源可以立即收费(例如,通过卡付款)或待处理(例如,需要客户采取行动的付款方式)。服务器端集成仍然是单个HTTP请求,该请求使用密钥创建计费。

每种付款方式的付款流程都依赖于相同的两个API抽象:来源和费用。乍一看,这在概念上似乎很简单,因为它类似于美国的卡集成。但是,一旦我们了解了这种流程如何集成到用户的应用程序中,便发现了许多粗糙的地方。

例如,当用户添加了一种无法立即完成的付款方式时,创建费用后,他们将无法立即履行其客户的订单。取而代之的是,他们必须等到费用转为成功后才能发货。这通常涉及添加一个侦听成功的webhook集成,然后在其中移动实现逻辑。

对于其他付款方式,来源和费用仍然更加复杂-集成问题可能导致收入损失。例如,使用荷兰主要的付款方式iDEAL,客户在将其重定向到银行的网站或移动应用后即可开始付款。如果客户端应用程序创建了源,然后浏览器失去了与服务器的连接,则即使客户认为已付款,下一次创建费用的请求也无法通过。 (由于多种原因,浏览器可能会失去连接性:客户在银行网站上付款后会关闭其标签页,该付款方式要求重定向,使该客户从不回头,或者客户的互联网连接不稳定。)服务器从未创建过费用,我们会在几个小时后退还与来源相关的款项。这是一场转换的噩梦。

为了减少这种情况的发生,我们建议用户从服务器轮询Stripe API,直到Source变为可收费状态,或者侦听source.chargeable webhook事件以创建Charge。但是,如果用户的付款应用程序出现故障,并且使用了“来源和费用”,则不会发送这些Webhook,服务器也不会创建费用。我们将退还客户的款项,用户必须将他们重新带回他们的网站才能再次付款。即使用户正确实施和维护了此最佳做法,围绕“来源和费用”的不同可能状态以及不同付款方式类型的路径和要求,仍然存在复杂性。

某些来源(例如卡和银行帐户)是可同步收费的,可以在提交付款表格后立即在服务器上收费,而其他来源则是异步的,只能在数小时或数天后才收费。用户通常使用同步HTTP请求和事件驱动的Webhook处理程序来构建并行集成,以支持每种类型。这意味着用户现在可以在多个地方创建收费并完成订单。对于OXXO之类的付款方式,代码分支因子会加深,客户可以打印出实际的凭证并将其带到OXXO商店以现金付款。钱是完全带外支付的,因此我们的最佳实践建议是监听这些付款方式绝对需要的source.chargeable webhook事件。最后,用户必须跟踪每个订单的费用ID和来源ID。如果同一来源的两个货源都可以收取费用(例如,客户决定将其付款方式更改为“中途付款”),则可以确保他们不会对该订单重复收费。

与“七行代码”相比,此工作需要开发人员更多的簿记和概念理解。我们的用户需要掌握所有这些极端情况,才能构建有效的Stripe集成。想象一下由于这两个状态机的推理而引起的混乱,每个状态的定义取决于付款方式!开发人员必须管理两个状态机的成功,失败和挂起状态(两个状态机的状态可能因不同的支付方式而有所不同),才能完成一次支付。

让我们再次参考付款方式表。您可能会注意到,卡是左上象限中唯一的付款方式:它们会立即完成,无需客户采取行动即可完成付款。这意味着我们在针对所有最简单的付款方式(卡)而设计的一组抽象之上,为新的付款方式提供了支持。自然,为卡设计的抽象在表示这些更复杂的支付流程方面将不会很出色。

引入其他状态并扩展为特定的狭窄用例创建的资源的定义,会导致混乱的集成和API抽象集的重载。好像我们试图通过在汽车上增加零件直到它具有宇宙飞船的功能来建造宇宙飞船一样:一个困难且可能注定的命题。收费和令牌是API中的基础,因为它们是我们拥有的第一个API,而不是因为它们是全球支付的正确抽象。我们需要从根本上重新考虑我们的付款抽象。

当我们取消对“来源和费用”的进一步更改时,我们就可以开始设计所需的API。这很容易,因为多年来我们有机会向用户学习,并且深刻理解了他们在现有集成路径中遇到的问题。我们还积累了支付领域的专业知识,在API的迭代方面拥有多年的经验。综上所述,我们的API设计有更好的机会不再重复过去的错误。

我们将自己锁在会议室三个月,目的是设计一个真正统一的支付API。如果成功,开发人员只需了解一些基本概念即可构建付款集成。即使他们没有听说过付款方式,他们也应该能够在集成的几个特定点上添加一些参数。为了做到这一点,我们API的状态和保证必须极其可预测和一致。我们的文档中不应散布一系列警告和异常。

由五个人组成的团队-四名工程师和一名PM-遍历了我们支持的每种付款方式,我们可以想象将来会提供支持。我们迭代了一个能够对所有模型进行建模的API设计。我们忽略了所有现有的抽象,而是从最初的原则开始考虑这个问题。

现在很难确切记住每天发生的事情,但是一些规则和例程确实对我们有所帮助:

关闭笔记本电脑。当在同一个房间里一起工作时,我们发现最好的完整出现的方法是关闭计算机。当我们这样做时,我们会感到更多的聆听,并且可以更清楚,更轻松地相互解释我们的推理。

处理您的问题。从每个会话开始,要回答一系列问题。写下下一届工作会议中出现的任何新问题。尽量避免在此刻讨论它们。在两次会议之间的时间里,您将与这些问题有所距离,收集新信息,并对有关该主题的内容进行更多的冥想。在每个会话结束时,给出明确的答案和问题,以在下一个会话中进行探讨。

使用颜色和形状。在早期,依靠简单的表示形式来表示复杂的新生概念,而不是尝试给它们指定具体的名称。我们用尽了可用的一组标记颜色,并在白板上绘制了许多形状。这种方法可以帮助我们避免为要塑造的概念确定特定的定义,并帮助我们避免过早命名自行车棚。

专注于实现真正的用户集成。在API设计中,追赶完美的不变式,不透气的理论或纯理论上的解决方案是很常见的,但是如果不能实现真正的用户集成,那么这些都没有用。我们的主要设计工具之一是编写假设性的集成指南,以验证我们的概念并确保我们不会引入新的或失败的陷阱。我们为我们可以列出的每种付款方式,甚至对于我们组成的某些付款方式(例如通过信鸽寄钱)编写了这些信息。

质疑支撑现有API的每个假设。我们专门设计了第一个API,以使卡支付极为简便,并且此后相对有机地增长了。我们需要动不动就从第一原则出发。回顾过去,我们可能可以做得更多。

邀请域专家作为来宾。导入专门知识以便在讨论时考虑特定主题。通过专业知识提升对话水平。

知道您可能会改变主意,因此迅速做出决定。新的观察结果或数据将进一步巩固我们的最初决定,或使我们做出更好的选择。在每种情况下,尽早做出决定并避免停滞都是更有效的方法,即使我们后来撤销了该决定。

我们常常觉得自己是在强行解决问题空间,但是任何大型设计项目的敌人都没有足够快地做出决策,因为没有选择是完美的。

我们最后有了两个新概念:PaymentIntents和PaymentMethods。通过打包这两个概念,我们最终设法为所有付款方式创建了一个集成。

与原始令牌一样,PaymentMethods表示有关客户要使用的付款方式的静态信息。它包括付款方案和转移资金所需的凭证,例如卡信息或客户的姓名或电子邮件。对于某些方法,例如支付宝,仅需要支付方式名称,因为在您重定向到支付方式的网站后,支付方式本身会处理进一步的信息收集。与“来源”不同,在PaymentMethod对象上捕获的特定交易没有特定的状态或数据,您可以将其视为指定如何处理付款的对象。

另一方面,PaymentIntents捕获特定于交易的数据,例如收费多少,并且是有状态对象,用于跟踪客户使用各种付款方式进行付款的尝试。结合使用PaymentMethod(“方式”)和PaymentIntent(“什么”),可以尝试付款。如果一次付款尝试失败,则客户可以使用其他PaymentMethod再试一次。

require_confirmation:“确认”基本上是指“赚钱!”有时,您可能需要在收集付款方式详细信息和实际赚钱之间停下来,而这种(可选)状态使这成为可能。

require_action:请执行指定的操作。从通用的redirect_to_url(不言自明)到非常特定于付款方式的操作(例如oxxo_display_details),它可以为您提供生成OXXO凭证的信息。

失败:没有失败状态,因为如果单次付款尝试失败,那么PaymentIntent会返回到require_payment_method状态,以便客户可以使用其他付款方式重试。这很方便,因为可以在客户端上重复使用在服务器端创建的同一对象。

借助Charges and Sources,针对卡,iDEAL和ACH借记卡的“最佳实践”付款集成需要管理两个webhook处理程序(一个对时间敏感,并且在正确收取钱的关键路径上),每次处理3次不同的Charge可能成功,处理两条失败的路径,并处理两个有状态的对象。

使用PaymentIntents和PaymentMethods,所有所有付款方式类型的集成都是相同的:首先在服务器上创建一个PayIntent,以获取订单的金额和货币。将嵌入在PaymentIntent中的秘密传递给客户端。收集客户的首选付款方式,并使用机密和付款方式信息确认PaymentIntent。 PaymentIntent会指示其处于require_action状态时的下一步操作。每个付款方式的行为都是标准化且可预测的;例如,通过一组操作来管理3D安全身份验证流程。最后,听取payment_intent.succeed Webhook或等待PaymentIntent进入成功状态,以了解何时保证有资金以及何时履行客户的订单。这完全由一个可预测的状态机管理。重要的是,对于转化而言,用户必须实施的唯一Webhook处理程序并不是募集资金的关键途径。

难于实现但有趣的是,设计一套可以在全球范围内跨所有付款方式使用的API。 Beta的生产就绪版本的API的实现也相对简单。但是,推出一个新的API来替代已建立的基础API并不仅限于编写符合规范的代码-推出

......