<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: aveeJ</title>
    <description>The latest articles on DEV Community by aveeJ (@j_8cd4e485ea10a59c2fb).</description>
    <link>https://dev.to/j_8cd4e485ea10a59c2fb</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4007604%2F03c9b679-21e2-475b-8763-2c1fde7a8dfb.png</url>
      <title>DEV Community: aveeJ</title>
      <link>https://dev.to/j_8cd4e485ea10a59c2fb</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/j_8cd4e485ea10a59c2fb"/>
    <language>en</language>
    <item>
      <title>From Authorization to Settlement: A Payment Is a Lifecycle</title>
      <dc:creator>aveeJ</dc:creator>
      <pubDate>Mon, 29 Jun 2026 18:37:23 +0000</pubDate>
      <link>https://dev.to/j_8cd4e485ea10a59c2fb/from-authorization-to-settlement-a-payment-is-a-lifecycle-5bii</link>
      <guid>https://dev.to/j_8cd4e485ea10a59c2fb/from-authorization-to-settlement-a-payment-is-a-lifecycle-5bii</guid>
      <description>&lt;p&gt;A payment isn't a thing you fetch. It's a thing that happens.&lt;/p&gt;

&lt;p&gt;A card gets authorized in one request. The money gets captured in another, maybe&lt;br&gt;
the next morning when the warehouse confirms stock. A refund shows up a week&lt;br&gt;
later. A 3DS challenge parks the whole thing in a "waiting on the customer" limbo&lt;br&gt;
that resolves in a browser tab your server never sees. Confirmations trickle in&lt;br&gt;
by webhook, out of order, sometimes twice.&lt;/p&gt;

&lt;p&gt;Most SDKs flatten all of that into a &lt;code&gt;Payment&lt;/code&gt; object with a single &lt;code&gt;status&lt;/code&gt;&lt;br&gt;
field. That shape is convenient, and it's exactly where things start to go&lt;br&gt;
wrong:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;succeeded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fulfillOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// looks fine, ships a bug&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's the catch: &lt;code&gt;"succeeded"&lt;/code&gt; isn't one thing. A succeeded authorization and a&lt;br&gt;
succeeded capture are different events, and a success you polled for is different&lt;br&gt;
again from one that turned up in a settlement webhook. Authorizing reserves the&lt;br&gt;
money. Capturing actually moves it. So if you fulfill on the authorization,&lt;br&gt;
you've shipped goods against a hold that can still be declined later, or just&lt;br&gt;
quietly expire.&lt;/p&gt;

&lt;p&gt;And the code passes review. The test card authorizes and captures in one go, so&lt;br&gt;
the demo is green and everyone moves on. Then a real customer pays with a 3DS&lt;br&gt;
card, the &lt;code&gt;else&lt;/code&gt; branch fires on a payment that actually went through, and you're&lt;br&gt;
debugging it in production.&lt;/p&gt;

&lt;p&gt;Good SDKs hand you the whole lifecycle. The ones that don't give you a noun with&lt;br&gt;
a status field, and let you write code that reads correctly and behaves wrong.&lt;/p&gt;
&lt;h2&gt;
  
  
  How Hyperswitch Prism models it
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/juspay/hyperswitch-prism" rel="noopener noreferrer"&gt;Prism&lt;/a&gt; doesn't hand you one mutable &lt;code&gt;Payment&lt;/code&gt; to poke at. A payment has a&lt;br&gt;
position in a state machine, with named flows instead of a single status. Its&lt;br&gt;
&lt;code&gt;PaymentStatus&lt;/code&gt; enum carries thirty-odd states, and they're grouped on purpose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;STARTED → AUTHENTICATION_PENDING → AUTHORIZED → CHARGED → (refund sub-process)
                  │                     │           │
            (3DS / SCA)          (money reserved)  (money moved)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API mirrors that machine. There's no status setter sitting on an object.&lt;br&gt;
The lifecycle shows up as verbs spread across a few clients, and every call tells&lt;br&gt;
you where the payment ended up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;PaymentClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                 &lt;span class="c1"&gt;// get / capture / refund / void&lt;/span&gt;
  &lt;span class="nx"&gt;MerchantAuthenticationClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// open the session, mint client token&lt;/span&gt;
  &lt;span class="nx"&gt;EventClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                   &lt;span class="c1"&gt;// parse + verify asynchronous webhooks&lt;/span&gt;
  &lt;span class="nx"&gt;types&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hyperswitch-prism&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;paymentClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;capture&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;connectorTransactionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice &lt;code&gt;capture&lt;/code&gt; is its own method, not &lt;code&gt;update({ status: "captured" })&lt;/code&gt;. You&lt;br&gt;
can't fat-finger a capture onto a payment that was never authorized and have it&lt;br&gt;
pass as a harmless-looking status string. The methods you can call are the&lt;br&gt;
transitions you're actually allowed to make.&lt;/p&gt;
&lt;h2&gt;
  
  
  One word, three enums
&lt;/h2&gt;

&lt;p&gt;A flat &lt;code&gt;status&lt;/code&gt; field is a trap because "succeeded" turns up in three different&lt;br&gt;
places that mean three different things. &lt;a href="https://github.com/juspay/hyperswitch-prism" rel="noopener noreferrer"&gt;Prism&lt;/a&gt; gives each one its own type:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;PaymentStatus.AUTHORIZED&lt;/code&gt; vs &lt;code&gt;PaymentStatus.CHARGED&lt;/code&gt; — reserved vs moved.&lt;/li&gt;
&lt;li&gt;Webhook events are a separate enum: &lt;code&gt;PAYMENT_INTENT_AUTHORIZATION_SUCCESS&lt;/code&gt; vs
&lt;code&gt;PAYMENT_INTENT_CAPTURE_SUCCESS&lt;/code&gt;. Both say "success." Only one means you got
paid.&lt;/li&gt;
&lt;li&gt;Refunds get their own enum again: &lt;code&gt;RefundStatus.REFUND_SUCCESS&lt;/code&gt;, with its own
pending and failure states, because a refund is its own sub-process.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cram all three into one &lt;code&gt;status&lt;/code&gt; field and you're overloading it, and the&lt;br&gt;
overloading is where the bug lives. Keep the flows apart and you read the one you&lt;br&gt;
actually meant.&lt;/p&gt;
&lt;h2&gt;
  
  
  Two non-PCI flows, two shapes
&lt;/h2&gt;

&lt;p&gt;"Non-PCI" means the sensitive part (the card number, the PayPal login) gets&lt;br&gt;
entered in the connector's own client-side SDK and never reaches your server.&lt;br&gt;
Your backend only ever holds tokens and ids. &lt;a href="https://github.com/juspay/hyperswitch-prism" rel="noopener noreferrer"&gt;Prism&lt;/a&gt; supports this, but the&lt;br&gt;
lifecycle looks different from one connector to the next, which is another reason&lt;br&gt;
a flat &lt;code&gt;status&lt;/code&gt; falls short. Take Adyen and PayPal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NON-PCI: the card / login is entered in the connector's client SDK.
Your server only holds tokens and ids — never raw card data.

ADYEN  ·  authorize now, capture later, outcome via webhook
  Browser Drop-in        Your backend           Prism / UCS        Adyen
       │                      │                      │               │
       │  open session ──────►│ createClientAuth ───►│ ── /sessions ►│
       │ ◄─ clientToken ──────│ ◄────────────────────│ ◄─ session ───│
       │                      │                      │               │
       │  card entered in Drop-in, authorized client-side ──────────►│
       │                      │ ◄═══ AUTHORISATION webhook ══════════│
       │                      │ EventClient.handleEvent (verify HMAC)│
       │                      │ record pspReference                  │
       │  complete checkout ─►│ authorizePayment                     │
       │                      │   polls verified outcome (~12s)      │
       │ ◄── AUTHORIZED ──────│   capture is a SEPARATE call later   │
       ▼                      ▼                      ▼               ▼

PAYPAL  ·  approve in popup, then auto-capture server-side
  Browser Buttons        Your backend           Prism / UCS        PayPal
       │                      │                      │               │
       │  open session ──────►│ create order ───────►│ ── /orders ──►│
       │ ◄─ orderId ──────────│ ◄────────────────────│ ◄─ orderId ───│
       │                      │                      │               │
       │  buyer approves in popup (no card on server) ─────────────►│
       │  complete checkout ─►│ authorizePayment                     │
       │                      │   PaymentClient.authorize            │
       │                      │   paypalSdk token + AUTOMATIC ──────►│ capture
       │ ◄── CAPTURED ────────│ ◄────────────────────│ ◄─ CHARGED ───│
       ▼                      ▼                      ▼               ▼
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;strong&gt;Adyen&lt;/strong&gt;, the card is authorized inside the Drop-in, and the result only&lt;br&gt;
reaches your server by webhook. So &lt;code&gt;authorizePayment&lt;/code&gt; can't just read a status.&lt;br&gt;
It waits for the verified event, then picks up the &lt;code&gt;pspReference&lt;/code&gt; from that&lt;br&gt;
webhook as the transaction id, which it needs for the capture you run separately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// initiate: hand the browser a client token; the card stays client-side&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;authClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createClientAuthenticationToken&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="cm"&gt;/* amount */&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="c1"&gt;// → { clientToken, publishableKey }  — server never sees the card&lt;/span&gt;

&lt;span class="c1"&gt;// authorize: the outcome only exists once the verified webhook lands&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;outcome&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getWebhookOutcome&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// recorded by EventClient&lt;/span&gt;
&lt;span class="c1"&gt;// → AUTHORIZED + pspReference   (capture is a later, separate call)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;PayPal&lt;/strong&gt; works the other way around. The order is created up front, the buyer&lt;br&gt;
approves it in the popup, and your server captures the approved order in one shot&lt;br&gt;
with &lt;code&gt;AUTOMATIC&lt;/code&gt; capture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// initiate: create the order server-side, hand the browser its id&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;orderId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createPaypalOrder&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="cm"&gt;/* amount, currency */&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="c1"&gt;// → buyer approves orderId in the PayPal popup; no card on your server&lt;/span&gt;

&lt;span class="c1"&gt;// authorize: capture the approved order using the JS-SDK token&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;paymentClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;authorize&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;paymentMethod&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;paypalSdk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;orderId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;captureMethod&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CaptureMethod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AUTOMATIC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="c1"&gt;// → CHARGED   (already captured; only refunds remain)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same vocabulary, two real paths. Adyen ends on &lt;code&gt;AUTHORIZED&lt;/code&gt; with a capture still&lt;br&gt;
owed; PayPal ends on &lt;code&gt;CHARGED&lt;/code&gt; right away. A plain &lt;code&gt;status&lt;/code&gt; field would call both&lt;br&gt;
of those "succeeded" and hide the fact that one of them still needs a capture.&lt;br&gt;
The named states won't let that slip.&lt;/p&gt;

&lt;h2&gt;
  
  
  Truth that isn't synchronous
&lt;/h2&gt;

&lt;p&gt;Sometimes the answer isn't there yet when you ask for it. In client-side flows&lt;br&gt;
the authorization decision lands later, over a webhook, so a synchronous read is&lt;br&gt;
really just a guess. &lt;a href="https://github.com/juspay/hyperswitch-prism" rel="noopener noreferrer"&gt;Prism&lt;/a&gt; treats it as a guess rather than pretending&lt;br&gt;
otherwise:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A status read with nothing behind it yet comes back &lt;code&gt;PENDING&lt;/code&gt;, not &lt;code&gt;ERROR&lt;/code&gt;.
Unknown means "check again in a bit," not "decline someone who already paid."&lt;/li&gt;
&lt;li&gt;An async event has to prove itself before it can move state. &lt;code&gt;EventClient&lt;/code&gt;
parses the webhook, checks the source, and only then maps it. A webhook it
can't verify is just a forged transition, so it gets rejected, and there's no
dev-mode shortcut around that.&lt;/li&gt;
&lt;li&gt;Even the amount isn't a plain number. It's a currency-scaled minor unit, so
&lt;code&gt;1000&lt;/code&gt; is ¥1000 in one currency and $10.00 in another.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Treat a payment as a lifecycle and a few habits follow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Don't branch on one &lt;code&gt;status&lt;/code&gt;.&lt;/strong&gt; Ask the specific question: authorized,
captured, or refunded? They're different flows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat a sync read as provisional.&lt;/strong&gt; Default unknowns to pending and let the
verified event settle it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never fulfill on authorization.&lt;/strong&gt; Only a capture means you've been paid.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Money is a currency-scaled integer&lt;/strong&gt;, not a number.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A payment runs from authorization to settlement, and treating it as a lifecycle&lt;br&gt;
rather than a single status is the gap between code that works and code that only&lt;br&gt;
looks like it does. &lt;a href="https://github.com/juspay/hyperswitch-prism" rel="noopener noreferrer"&gt;Prism&lt;/a&gt; puts the&lt;br&gt;
lifecycle in front of you, with separate verbs, separate flows, and events it&lt;br&gt;
actually verifies, so the obvious thing to write tends to be the right thing too.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Reference:&lt;/strong&gt; &lt;a href="https://github.com/juspay/hyperswitch-prism" rel="noopener noreferrer"&gt;Hyperswitch Prism on GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>prism</category>
      <category>hyperswitchprism</category>
      <category>opensource</category>
      <category>payments</category>
    </item>
  </channel>
</rss>
