PhonePe · Payment gateway

The SDK that merchants bet their checkout on

My first project at PhonePe: I owned the B2B payment gateway SDKs end to end, and wrote the Java one.

  • Java
  • Open source
  • API design
  • Payments
  • Prior art: Stripe
  • Funnel analytics
  • Batched telemetry
  • Design docs
  • Tens of crores in TPV

Why an SDK at all

PhonePe's B2B payment gateway onboards external merchants who put our payment APIs inside their own checkout. Before the SDK, every one of them integrated against raw REST: reading the docs, hand-rolling request signing, auth, retries, error handling.

Every merchant re-derived the same solution, and every merchant got it slightly wrong in a different way. Those mistakes did not stay theirs: an integration bug surfaces as a customer failing to pay, which is a PhonePe problem and a support ticket and a merchant who is now less sure about us. Integration friction was a direct drag on merchant activation.

So the SDK was not a convenience library. It was the golden path, made hard to leave.

The point of it is what a merchant's engineer doesn't have to think about. Endpoints, request signing, auth, retries, error handling, the shape of every payload: all of it abstracted away. None of it is their problem, because none of it is their business. A merchant integrating a payment gateway wants to think about their own business logic: what they are charging, for what, and what to do when it succeeds or fails. Everything between that intent and our servers is plumbing, and plumbing is what an SDK is for.

It's now public, and tens of crores in total payment volume have gone through the SDK family.

The PhonePe/phonepe-pg-sdk-java repository on GitHub: public, Apache-2.0 licensed, on Maven Central, with Shubhransh Jagota listed as a committer
Public, Apache-2.0, on Maven Central

My first project at PhonePe

This was my first project at PhonePe. I wanted to do everything the thing needed in order to succeed, and I was given the room to, because I was handed it end to end.

Not "write the Java client". All of it:

  • Design: I designed the entire library.
  • Implementation: I wrote it.
  • Documentation: the thing a merchant's engineer actually reads first.
  • Testing.
  • CI/CD: the pipelines that build and ship it.
  • Release, including, eventually, the open-source one.

And beyond the Java one: every decision about the SDK offering went through me. What the SDKs did, how they behaved, what a merchant could expect from any of them. That was mine to get right or get wrong.

A few of those decisions are worth naming, because they are the ones a merchant never sees and always feels.

  • Singletons, abstract classes, factory builders. The merchant asks for a client and gets one, correctly configured. They assemble nothing.
  • The seams are for us, not them. When auth changes (and auth always changes) the implementation moves behind the factory, and nobody's integration notices.
  • Events go into an in-memory persistent queue, shipped to our events API in batches. Never inline with the payment call: a merchant's checkout does not wait on our analytics.
  • Telemetry is switchable off at initialisation. Some merchants run on genuinely constrained resources, and our curiosity is not their problem.

So the first thing I did was read somebody else's homework. Stripe and the other giants had solved this years earlier, in public. I went through their SDKs, not to copy the shape, but to work out why each thing was shaped the way it was. Those decisions only look arbitrary until you understand what they are defending against, and somebody had already paid for the lesson and published the answer. Not reading it would have been vanity.

Woody and Buzz Lightyear meme: 'Let's write the SDK — promotion guaranteed'

Multiple SDKs, one behaviour

I owned the SDK offerings, not just the Java one. The Python, .NET and JavaScript SDKs were built by three or four engineers (juniors and interns) and I mentored them rather than writing the SDKs myself.

The trap there is obvious in hindsight and invisible at the time: several SDKs written by several people become several dialects. Same gateway, different ideas about what an error means, different retry policies, different names for the same field. So the gate went in at the design level: get the behaviour agreed before anyone opens an editor, and the implementations converge on their own.

Same same, but different

Two things made that hold in practice. A design document, published for anyone to follow while writing their SDK, so the shared structure was written down rather than living in one head and being rediscovered in review. And code review across the language offerings, to keep that basic structure consistent, because a document tells people what to do, and a review is how you find out whether it happened.

The launch broke

At launch, our first merchants' transactions were blocked by the fraud and risk service. The cause was small and stupid, as these always are: a missing merchant-id header.

A panda covering its face in shame and regret

Support surfaced it, and I rolled a fix within a few hours. I also folded the regulator's requested guidelines into the SDK.

We had tested against internal, whitelisted merchants, and not all of the checks apply to whitelisted merchants. The exact path that broke in production was the one path our test setup was structurally incapable of exercising. Every test passed, and every test was blind in the same place.

So the fix I actually care about is that I wrote it down. I documented the check: what triggers it, why whitelisted merchants never see it, and what an integration has to send. The next person to build against this does not get to discover it the way we did, which is the only version of this story that is worth anything to anyone but me.

How we knew it was working

Every number in this post came from somewhere, and building that somewhere was part of the job.

Yoda and Luke Skywalker, captioned: 'If you can't measure it, you can't improve it' — Yoda

Those events gave us the integration funnel: where a merchant's engineer picked the SDK up, where they got stuck, where they dropped, and where they finally put a live transaction through. A funnel turns "integration is painful" (which is an opinion) into a specific step that loses people, which is a thing you can go and fix.

On top of the events I built an internal dashboard with the analytics around it: weekly and monthly success rates, so adoption was something we watched rather than something we assumed. If a release made things worse, we would see it in a week instead of hearing it in a support ticket a month later.

Integration in under a day

Once the surface was small enough, the code-level integration came down to under a day. Not because anyone got faster, but because there was much less to get right.

< 1 day code-level integration,
down from weeks
~50% fewer integration
issues

Making it public

Once the Java SDK was solid, it was living in an internal repository, serving only the merchants already in our sales funnel. That felt like a waste of the thing.

A public SDK is a channel. It reaches developers who will never talk to a salesperson, and it lets a merchant's engineers read the code before they commit to the payment gateway that will handle their money, which is exactly what a good engineer does before making that decision.

So I drove the open-source release: appsec approval, the architects' approvals, and the public repository. It is on GitHub at PhonePe/phonepe-pg-sdk-java.

The Java SDK went out first, and the other language SDKs followed it into the open after.

That is the part I am most glad about. The work is out where the world can see it and critique it, open to everyone. It also means that when I tell you I designed a payment SDK that merchants bet their checkout on, you do not have to take my word for it. You can go and read it.