Back to all posts
InvopopStripeAppLearningProduct Development

My Rollercoaster Ride Building the Invopop Stripe App

10 min read

When I rejoined Invopop, I was handed a challenge that perfectly encapsulates the company's spirit: "Build an Invopop app for the Stripe marketplace." At Invopop, freedom is abundant, and with it, the responsibility to deliver. Sam and Juan simply told me, "We want this integration. We've never done anything like it, and we believe in you." As someone who thrives on challenges, I was all in—despite having no prior experience with Stripe app development, or even a clear use case in mind.

This journey was a masterclass in learning, filled with unexpected turns and valuable insights.

Invopop App in Stripe

Invopop App in Stripe


The Hard Truths: Asking More and Researching First

My first major learning came quickly: ask more. My default has always been to tackle problems solo. If I hit a wall, I find a way over it. While admirable, this approach can be a hindrance when surrounded by experienced minds like Sam, Juan, and our designer, Mark. I quickly realized that not asking questions meant going slower and making unnecessary mistakes. Collaboration isn't just about teamwork; it's about leveraging collective wisdom to accelerate progress.

Another early stumble was my tendency to dive headfirst into building without adequate research. As an engineer, I love getting things done and starting ASAP. But this project hammered home the importance of upfront planning, thorough research, and a solid design. For instance, I initially neglected to research similar apps in the Stripe marketplace. In hindsight, this was a rookie error; these apps could have served as invaluable benchmarks. I briefly looked at A-cube and Billit, but their confusing UX and obscure data requirements (like a "codice fiscale") quickly led me to give up. Had I invested more time in understanding the landscape, defining the use case, and designing the user experience, I'm convinced I could have shaved a month off the development time. It's true what they say: you don't truly learn until you stumble. Now, I know better.


The Core Technical Hurdles

This project was inherently challenging for several reasons:

  • External Integration: Unlike our internal Invopop apps, this was our first application built entirely outside our ecosystem, yet it needed to communicate seamlessly with Invopop.
  • Front-End Heavy: As someone less inclined towards front-end development, building the complete UI from scratch within the Stripe environment was a significant undertaking. Organizing the UI, making it match the designs precisely—it all proved more complex than anticipated.
  • Authentication Odyssey: Handling user authentication directly from Stripe into Invopop via OAuth was a complex but incredibly rewarding learning experience. While Sam had laid much of the groundwork with our existing OAuth system, the intricacies of authenticating external requests presented a steep learning curve.
  • Stripe Platform Limitations: We're UX fanatics at Invopop, dreaming of an experience where users never have to leave Stripe. However, some essential features, including login, workflow selection, and billing, still necessitate logging into the Invopop console. We are continuously working to bring these functionalities directly into Stripe.
  • Stripe accounts to Invopop workspaces: Stripe Accounts to Invopop Workspaces: A significant challenge was accurately mapping Stripe accounts to their corresponding Invopop workspaces. This involved not only identifying which Stripe account was linked to which Invopop workspace but also discerning the Stripe environment (live, test, or sandbox) from which requests or events originated.

The GOBL.Stripe Conversion: The Project's Backbone

The very first piece I built, and arguably the core of the entire project, was the conversion from Stripe's data format to our internal GOBL format. This process allows invoices from Stripe to be seamlessly understood and processed by Invopop. While it sounds straightforward, converting data between formats is always tricky. You encounter numerous decisions, especially when fields don't have a direct equivalent. For instance, Stripe doesn't have a dedicated "supplier" field; the supplier is the Stripe account holder. This meant I had to extract supplier information from various account fields, building a robust logic to handle scenarios where certain fields were empty. This alone consumed about two weeks.

You can find the conversion code here as it is open source: gobl.stripe

GOBL.Stripe

GOBL.Stripe

The "Internal" Stripe App: A First Step

To get something functional quickly, I initially built an "app" within Invopop that could import invoices from Stripe. This mirrored our existing Chargebee integration: users would paste their API key and webhook secret into the app configuration, enabling us to listen for events. While straightforward to implement, the UX wasn't ideal, requiring users to manually create and copy keys.

Looking back, this could have been a viable stopping point to validate demand before committing to a full-blown Stripe marketplace app. While the marketplace app offers superior UX, flexibility, and brand presence, the two additional months spent building it might have been better invested elsewhere at that stage. This reinforced a crucial lesson: ship something useful, but ship fast. Leading the project, I should have recognized this potential over-engineering earlier.

Invopop internal apps

Invopop internal apps

This phase brought its own set of challenges:

  • Managing Event Reception: Webhook event reception is deceptively complex. Webhook endpoints are designed for rapid responses (a quick 200 OK), or an error to trigger a retry. The challenge arises when your system can't process a high volume of events synchronously. The solution? An event queue. We'd queue the event, send a 200 OK to Stripe, and then process the request asynchronously. However, this introduces the "limbo" problem: what if processing fails after you've told Stripe everything's fine? Our initial solution: increase reliability by making the webhook wait for processing (even if it risks duplicate sends, which we handle with IDs).
  • Invoice Calling: When and How: Stripe's data is highly normalized. An invoice.finalized webhook payload contains only IDs for related objects like customers, payment intents, and line items. To get a complete invoice, we needed to make an additional API call with an expand field to get all the necessary related data (for our case, there are six key fields we expand). The real question became: when to make this call? Given the need for fast webhook responses, an immediate API call could cause timeouts. However, our initial workflow required a complete invoice for a valid "silo entry." The solution came from a paradigm shift at Invopop: we now allow empty or invalid silo entries to kick off a workflow. A month later, we could even create a job with no silo entry, passing the Stripe invoice ID in metadata to fetch the invoice during workflow execution. This decoupled the webhook response from the full invoice retrieval.

The Grand Expansion: Building the App in Stripe

By early February, less than a month after starting, we had a functional (Invopop-internal) Stripe integration. Why then, two more months? Because we significantly underestimated the complexity of building the actual app within Stripe.

Building the Stripe app meant designing an entirely new UX. Users wouldn't be interacting with the Invopop console; they'd be using a sidebar within Stripe, which presented a unique set of design and development challenges. While Sam's foundational work makes building apps within Invopop relatively straightforward (handling authentication, inter-element communication), an external app demands managing its own authentication and secure communication with all Invopop services (access, silo, workspace fetching, workflow fetching)—all requests to our backend needed to be authenticated as coming from Stripe.

Invopop app login view

Invopop app login view

The Authentication Flow: From OAuth to Enrollments

My first week in this phase was dedicated to researching Stripe app architectures, authentication methods, and the OAuth process. For me, this was the most critical UX feature. Allowing users to connect via OAuth meant a single click could establish the connection and grant authorization to their Stripe account, eliminating the need for manual API key and webhook secret entry. Of course, providing a clear logout/disconnect option was equally crucial.

Now, with our Stripe app connected to the Invopop platform, we needed a way for the external Stripe app to communicate with the internal Invopop Stripe app, sharing data and triggering actions like invoice imports. This is where enrollments came in. Each Invopop app has its own enrollment, and to connect, you need an enrollment token. Typically, this token is generated when you click "connect" in the Invopop console. But for an external app, we needed a new approach: a new API endpoint to create an enrollment in a workspace from Stripe. This allows the Stripe user to simply select a workspace, and an enrollment is automatically created, returning a token for future authenticated requests.

Invopop app workspace selection

Invopop app workspace selection

The Webhook Dilemma: Mapping Account IDs to Workspaces

With authentication and enrollment handled, the next hurdle was the webhook. Our previous internal app used custom webhooks with the enrollment ID embedded in the URL. But Stripe's marketplace apps use a single, unique webhook endpoint for all requests. How do you know which account is sending the request? The account_id field in the webhook payload provides this. The new problem: how do we map that `account_id` to the correct enrollment (or workspace)?

My initial solution was a relational database storing account_id to enrollment_id mappings. While functional, it wasn't ideal for long-term maintenance. This led to a more elegant solution leveraging existing Invopop structures. When a user selects a workspace, we not only create an enrollment but also a supplier party in Invopop containing information from Stripe. This supplier has a metadata field storing the Stripe account_id. So, when a webhook arrives, we search for the supplier with that account_id to retrieve the associated workspace ID and enrollment.

This presented new edge cases. What if two enrollments point to the same Stripe account (e.g., live and sandbox environments)? Stripe's account_id is the same for both. Our solution: enforce that live Stripe accounts can only connect to live Invopop workspaces, and test/sandbox Stripe accounts connect to sandbox Invopop workspaces. When a user disconnects, we also need to delete this metadata to prevent conflicts.

Supplier metadata

Supplier metadata

The User Flow & UX Evolution

Now, the user flow was becoming clear (and required a significant amount of UI coding, remember my aversion to front-end!):

  1. User installs the app.
  2. User authenticates with Invopop via OAuth.
  3. User selects the Invopop workspace to connect to.
  4. User selects the workflow to send invoices to.
  5. Done, ready to go!
Invopop App settings page

Invopop App settings page

Every time a Stripe invoice or credit note is finalized, a job is executed for the selected workflow, and the invoice magically appears in Invopop. Success! Or so I thought. The "last 20% of work takes 80% of the time" adage held true.

By early March, the app could sync invoices, but the UX was, frankly, subpar. There was no information about the invoice's state, workflow execution, or even download options. The onboarding buttons were confusing. This led to a crucial conversation with our designer, Mark, the "UX magician." His designs were transformative, not just for the front end, but for the backend too. A clear user journey clarifies how all system elements should interact.

The project also grew in complexity with numerous edge cases appearing in testing, leading to a flurry of bug fixes. We also needed to support different environments (live, test, sandbox), each requiring distinct API keys and webhook endpoints—a considerable burden to manage.

For weeks, I focused on refining the front end, making it visually appealing without breaking existing functionality. Initially, we deemed the app's settings page useless. However, it quickly became apparent that users were redirected there upon installation. This necessitated building out a robust settings page UI as well, a process made easier by the clearer designs.

One significant change involved storing job execution information. Users needed to see the status of their invoices within Stripe. This required storing minimal job details (account ID, job ID) in a database and fetching full job information from Invopop on demand. The UI then displayed execution states, steps, linked Invopop invoices, and even attachments—a huge UX improvement. This particular feature, built at the end of March, reinforced the importance of storing only essential data and fetching the rest via API calls.

Invopop App workflow execution details

Invopop App workflow execution details


The Finish Line (and Beyond)

This first "clean" version was completed on April 11th, roughly three months after I started. In retrospect, we could have shipped an earlier, less polished version (like the Invopop-internal app) to gather user feedback sooner. This would have provided valuable insights into actual usage and potential issues, guiding our development more effectively.

One particularly interesting issue arose from Stripe's new sandbox environment: there was no way to detect if a front-end API call originated from sandbox vs. test. My initial "hack" involved trying a request to test and, if it failed with a specific error, retrying in sandbox. However, what truly impressed me was Stripe's response when I shared this feedback (along with other developer experience suggestions like managing multiple keys). They not only agreed but developed and shipped the feature to identify sandbox events within two weeks, informing me personally! It was a fantastic demonstration of a large company's dedication to developer experience.

Once the app was developed, creating the listing page on the Stripe Marketplace and undergoing the quick (one-day!) revision process were the final steps. Crafting compelling wording for the listing was crucial to attracting users interested in e-invoicing. You can see it live here: Invopop App in Stripe Marketplace

Invopop App in Stripe Marketplace

Invopop App in Stripe Marketplace


Conclusion: Learnings from the Build

Building the Invopop Stripe app was an immense learning experience, both technically and business-wise. While there were moments I felt time might have been misspent on certain complexities, the hindsight is always 20/20.

It was a great experience, filled with valuable learnings, even if I sometimes felt I could have spent the time on more "profitable" things. But that's the nature of innovation.

My main personal and product development takeaways from this journey are clear:

  • Don't Be Afraid to Ask (Early and Often): My initial instinct to "figure it out myself" often slowed me down. Don't underestimate the power of collective knowledge.
  • Prioritize Upfront Research and Design: Jumping straight into coding felt productive, but it was a false economy.
  • Ship fast, even if it's not perfect: Get a working version out there, gather feedback, and iterate quickly.
  • The 80/20 Rule is Real (Especially with UX): Getting to 80% functionality is often quick; the last 20% of polish, edge cases, and user experience refinement can take the vast majority of the time.
  • Embrace Paradigm Shifts: Invopop's willingness to rethink core workflows (e.g., allowing empty silo entries for jobs) enabled elegant solutions to challenging problems.

From a technical standpoint, the project provided invaluable lessons:

  • Complexity of External Integrations: Building an app that lives outside your core platform but interacts deeply with it introduces a whole new layer of architectural and security challenges (authentication, cross-service communication). Anticipate these complexities and design for them from the start.
  • Webhooks Are Harder Than They Look: Handling event reception, especially with varying processing times and reliability needs, is far more intricate than just setting up an endpoint. Considerations like queuing, asynchronous processing, and robust error handling for "limbo" states are critical for a resilient system.
  • Authentication is a Beast (But Worth Taming): Navigating OAuth flows, managing external authentication, and securely linking external accounts to internal entities (like our app enrollments) was one of the most complex but ultimately rewarding parts. A well-designed authentication system is foundational for a good user experience and system security.
  • Managing Databases is Tricky: Even for seemingly simple data mappings (like account_id to enrollment_id), choosing the right storage solution, handling edge cases (like multiple enrollments for the same account across environments), and ensuring data consistency adds significant complexity.

I hope sharing my journey provides valuable insights and inspires others to embrace the challenges of building and learning in the exciting world of product development.