# How I booked my IndiaFOSS ticket at the thirteenth hour

*Anas Khan, September 2026*

I have attended IndiaFOSS for the past four years. It is one of the few conferences I plan to attend before I know the schedule, because the people and the conversations are reason enough to go.

This year, despite following the event and knowing that I wanted to be there, I forgot to buy a ticket.

At 12:40 am, I remembered that I still had to buy a ticket, but it was too late. The ₹1500 Late Bird tier was no longer available through the normal booking flow, and the next obvious option was a ₹3000 Regular ticket. By the time I was done, it was 1 am, hence the title "thirteenth hour" :P

Luckily, I had been experimenting with something earlier, and that gave me an idea that might work. Read on to find out what happened and, if you have not worked with payment integrations before, learn a little about them along the way. This is a case study and a practical introduction to payment orders, embedded checkout, server-side amount validation, signatures, callbacks, and fulfillment.

![The IndiaFOSS ticket page with Late Bird unavailable and the Regular tier available](https://cdn.hashnode.com/uploads/covers/624dca9b375c2653317a1bc3/722ed692-4287-4d58-a065-33a34e78a520.png align="center")

*The Late Bird window had closed around forty minutes earlier.*

## A little background

I am a software engineer and tinkerer. I have worked with payment integrations before and usually get excited about that space.

At **FamPay**, I worked on fintech backend systems, including recharge and merchant-payment flows. At my last job at **HackerRank**, I worked across payments and other product areas, including subscription and checkout services using **Razorpay and Stripe**. I spent a fair amount of time building and debugging everything myself (a nightmare sometimes), so I had a little experience there.

I had also examined payment and webhook behavior while studying Interview Coder's source code. That allowed me to replay a webhook and get premium accounts for free. I wrote about it here: https://blog.anaskhan.me/decoding-interview-coder.

There was one more helpful coincidence. FOSS United's platform is built on **Frappe**, and I previously interned at Frappe Technologies. Once I found the relevant backend methods, their conventions were familiar.

None of this meant I knew the answer in advance. It meant I knew which questions to ask:

1.  Had FOSS United already created a Razorpay order for ₹1500?
    
2.  Was that order still payable after the ticket tier disappeared from the UI?
    
3.  If payment succeeded, how did FOSS United verify and fulfill it?
    

## The payment model: order, checkout, verification, fulfillment

Before describing what I changed, it helps to understand a standard Razorpay integration.

### 1\. The merchant creates an order

The merchant's backend decides what the customer is buying and calculates the amount. It then asks Razorpay to create an order.

The `order_id` identifies a specific payment attempt. The amount is set when the backend creates the order. A customer should not be able to edit the page and turn ₹1500 into ₹15 because the trusted amount lives on the server and at Razorpay.

### 2\. The browser opens Checkout

The frontend loads Razorpay's Checkout JavaScript and gives it the public merchant key and order ID.

```javascript
const checkout = new Razorpay({
  key: "rzp_live_...",
  order_id: "order_...",
  name: "FOSS United",
});

checkout.open();
```

The key ID is public by design. It identifies the merchant account. The corresponding secret must remain on the server.

### 3\. Razorpay returns a signed result

After a successful payment, Checkout returns three important fields:

```json
{
  "razorpay_order_id": "order_...",
  "razorpay_payment_id": "pay_...",
  "razorpay_signature": "..."
}
```

The signature binds the order and payment together. The merchant backend verifies it using the private Razorpay secret.

### 4\. The merchant fulfills the purchase

Only after verification should the application mark the payment as captured and provide the purchased item. For FOSS United, fulfillment means creating an event ticket, making the PDF available, and sending the confirmation email.

The order and payment move through related but separate state machines.

Stripe's PaymentIntent flow follows a similar separation, although the APIs and terminology differ.

![How FOSS United creates a Razorpay ticket order](https://cdn.hashnode.com/uploads/covers/624dca9b375c2653317a1bc3/fb4b335a-fcb1-438c-9e36-b6374c3388ee.png align="center")

*The browser initiates the flow, but the backend and payment provider establish what was actually paid.*

## What the coding-agent experiment left behind

A couple of days back, I had asked my coding agent to navigate the ticket flow for fun. I wanted to see whether an agent could inspect availability, select the tier, fill attendee information, and reach checkout.

It reached the correct boundary: the Razorpay payment screen. The agent could prepare the transaction, but I still had to approve the payment.

By the time I returned after midnight, the official page no longer allowed me to select Late Bird. The unfinished flow had nevertheless created an order ID for that tier.

An order ID is not a payment link. It cannot be pasted into a browser by itself. However, it might be useful somewhere else. I just needed to figure out where. Since I had worked on similar embedded pages at HackerRank, I suspected that reconstructing the missing final screen without changing the order or its amount might work.

First, I needed to confirm how FOSS United implemented the flow.

## Reading FOSS United's open source implementation

FOSS United is open sourced and its platform is hosted on GitHub. I followed the payment path through three files:

```text
dashboard/src/pages/BuyTickets.vue
dashboard/src/components/common/RazorpayCheckout.vue
fossunited/api/dashboard.py
```

### Server-side amount calculation

The first backend function I cared about was `create_razorpay_order` and the helper it used to calculate the total.

The implementation recomputed the amount using ticket-tier prices stored in the database. It also included optional T-shirt charges where applicable. It compared that server result with the amount provided by the client and rejected mismatches.

In simplified form:

```python
amount = compute_order_amount(ticket_tiers, attendees)
client_amount = checkout_info["amount"]

if amount != client_amount:
    raise ValidationError("Amount mismatch")

order = razorpay.order.create({
    "amount": amount_in_paise,
    "currency": "INR",
})
```

The backend then stored a pending payment record containing:

*   the Razorpay order ID
    
*   the calculated amount
    
*   the buyer email and billing information
    
*   the selected tier and attendee metadata
    
*   a `Pending` status
    

This was important for two reasons. I could not manipulate the browser into buying the tier for a different price, and the attendee information had already been associated with the pending order.

![Server-side Razorpay order creation code](https://cdn.hashnode.com/uploads/covers/624dca9b375c2653317a1bc3/a77ad7c1-141a-47b4-a293-bdfd0e3fdcff.png align="center")

*FOSS United recalculates the amount from database-backed tier prices before creating the Razorpay order.*

### Embedded Razorpay Checkout

Next I read `RazorpayCheckout.vue`. The component loaded Razorpay's Checkout script and created a popup using the returned public key and order ID.

Its success handler sent the Razorpay response to FOSS United:

```javascript
handlePaymentSuccess(response) {
  post("handle_payment_success", {
    order_id: response.razorpay_order_id,
    payment_id: response.razorpay_payment_id,
    signature: response.razorpay_signature,
  });
}
```

This established that FOSS United used an **embedded checkout**, not a permanent Razorpay payment link for each ticket.

The distinction is useful:

| Checkout type | How it works |
| --- | --- |
| Hosted payment link | The customer visits a Razorpay-hosted URL such as an `rzp.io` link |
| Embedded checkout | The merchant loads Checkout.js and opens Razorpay inside its own page using an order ID |

FOSS United's original buy page was no longer able to recreate my Late Bird selection, but the embedded checkout itself only needed the existing order ID and public merchant key.

![RazorpayCheckout Vue component](https://cdn.hashnode.com/uploads/covers/624dca9b375c2653317a1bc3/5c4efc92-5fe5-4f7a-ac90-2959d55e2eb0.png align="center")

*The official frontend opens Checkout with an order ID, then posts Razorpay's signed response to FOSS United.*

### Signature verification

Finally, I read `handle_payment_success` in the backend. It passed the order ID, payment ID, and signature to Razorpay's official verification utility. Only after successful verification did it update the pending payment to `Captured`.

In simplified form:

```python
razorpay_client.utility.verify_payment_signature({
    "razorpay_order_id": order_id,
    "razorpay_payment_id": payment_id,
    "razorpay_signature": signature,
})

payment.status = "Captured"
payment.payment_id = payment_id
payment.save()
```

This was the security boundary. Calling the endpoint without a real payment would not work because I could not generate a valid signature. The experiment still required me to pay the exact amount attached to the existing order.

![Razorpay signature verification code](https://cdn.hashnode.com/uploads/covers/624dca9b375c2653317a1bc3/c0570758-c345-46ea-a858-cb0ab5155628.png align="center")

*The backend verifies Razorpay's signature before marking the payment as captured.*

## Reconstructing the checkout page

After reading the code, I asked the agent to generate the smallest possible HTML page that mirrored the official Checkout component.

The page needed:

*   Razorpay's Checkout.js script
    
*   FOSS United's public Razorpay key ID
    
*   the existing Late Bird order ID
    
*   a handler that displayed the payment response
    

The core looked like this:

```html
<!doctype html>
<html>
  <head>
    <script src="https://checkout.razorpay.com/v1/checkout.js"></script>
  </head>
  <body>
    <button id="pay">Pay ₹1500</button>
    <pre id="result"></pre>

    <script>
      document.querySelector("#pay").onclick = () => {
        const checkout = new Razorpay({
          key: "rzp_live_...",
          order_id: "order_...",
          name: "FOSS United",
          description: "IndiaFOSS Late Bird Enthusiast",
          prefill: {
            name: "Anas Khan",
            email: "...",
          },
          handler(response) {
            document.querySelector("#result").textContent =
              JSON.stringify(response, null, 2);
          },
        });

        checkout.open();
      };
    </script>
  </body>
</html>
```

No private credentials were present. This page could only ask Razorpay to open an order that FOSS United had already created.

I opened it locally, waited for it to load, and clicked **Pay ₹1500**. The UPI QR code showed up. I thought the payment might fail or the order might have expired, but I grabbed my phone and tried anyway. Surprisingly, it went through.

That confirmed the key hypothesis: the Late Bird tier had expired in the storefront, but the existing Razorpay order was still open.

![Local checkout page with masked order and email](https://cdn.hashnode.com/uploads/covers/624dca9b375c2653317a1bc3/4dc463f7-6612-4e2e-b173-87838924fc6e.png align="center")

*A minimal local page reproduced the embedded checkout used by the official frontend.*

![Razorpay Checkout showing FOSS United and a masked order](https://cdn.hashnode.com/uploads/covers/624dca9b375c2653317a1bc3/2933b3eb-0b6c-48f6-8a24-d07e93bfaa10.png align="center")

*Razorpay still recognized the existing ₹1500 order after the ticket tier had disappeared from the UI.*

## Completing the payment and callback

Then I made the real ₹1500 payment.

Once Razorpay captured the payment, I still wasn't sure whether I would get the tickets. I thought I might end up with a failure, which would have been a fun little mess. But, to my surprise, it worked. Here's what happened under the hood: two independent completion paths were available, a server-to-server webhook and a browser success callback. They carry different signatures and solve different reliability problems, but both reconcile the same pending order in FOSS United's database.

### The webhook path

A **webhook** is an asynchronous HTTP request from one service to another when an event occurs. In this case, Razorpay can send FOSS United a `payment.captured` event after the charge is captured. This happens between servers and does not depend on my checkout tab remaining open.

The request includes the payment entity and its `order_id`. It also carries an `X-Razorpay-Signature` header. FOSS United computes an HMAC over the raw request body using the webhook secret configured in its Razorpay settings. If the signatures do not match, the event is rejected.

After verification, FOSS United's webhook handler:

1.  Writes a webhook log for auditability.
    
2.  Looks up the `order_id` in its `Razorpay Payment` table.
    
3.  Ignores unknown orders rather than creating records from untrusted input.
    
4.  Changes the matching payment from `Pending` to `Captured` for a `payment.captured` event.
    
5.  Saves the document, triggering Frappe's `on_update` hook.
    
6.  Creates the ticket only if one has not already been created for that payment.
    

Razorpay recommends webhooks as the reliable source of asynchronous payment state. If the customer closes the tab, loses connectivity, or never reaches the merchant's success page, the server can still reconcile the payment.

### The browser callback path

Razorpay accepted it and returned the expected payload to my handler:

```json
{
  "razorpay_payment_id": "pay_...",
  "razorpay_order_id": "order_...",
  "razorpay_signature": "..."
}
```

This is a separate Checkout signature, calculated from the order ID and payment ID using FOSS United's Razorpay key secret. The browser can carry the values, but it cannot generate a valid replacement signature.

Because this was a minimal page rather than the full FOSS United application, the normal post-payment experience did not complete cleanly. The popup remained visible and no ticket page appeared automatically. The payment result itself was available, though.

The official frontend would have posted those three fields to `handle_payment_success`, so I made the equivalent request manually:

```bash
curl 'https://fossunited.org/api/method/…handle_payment_success' \
  -H 'content-type: application/json' \
  --data-raw '{
    "order_id": "order_...",
    "payment_id": "pay_...",
    "signature": "..."
  }'
```

I did not manually trigger the webhook. I reproduced the **browser success callback** that the official FOSS United frontend normally makes after Checkout returns.

The endpoint returned HTTP 200 with an empty JSON object:

```json
{}
```

That initially looked ambiguous, but it matched the Frappe method's implementation: it completed its work without returning a response object. There was no signature error, and the pending payment had been captured.

The callback and webhook can arrive in either order. A robust payment integration must expect that race. FOSS United converges both paths on the same `Razorpay Payment` document, and its ticket hook checks whether a ticket already exists for that payment before creating one. That makes fulfillment idempotent: replaying a valid callback or receiving the webhook later should not mint another ticket.

I then opened FOSS United's payment-success route with the order ID, payment ID, and event ID. The ticket appeared, I downloaded its PDF, and the confirmation email arrived a few minutes later.

![](https://cdn.hashnode.com/uploads/covers/624dca9b375c2653317a1bc3/938a09e1-e4e4-4003-9f12-6aeddbd6dad9.png align="center")

*Checkout returned the signed values needed by FOSS United's payment-success endpoint.*

![](https://cdn.hashnode.com/uploads/covers/624dca9b375c2653317a1bc3/00329526-d809-4d4f-94de-42409b4e53ad.png align="center")

*The request reproduced the callback made by FOSS United's official frontend.*

![](https://cdn.hashnode.com/uploads/covers/624dca9b375c2653317a1bc3/df20fb76-297f-45f6-95de-cb1ebcfee7ec.png align="center")

*The browser callback gives an immediate result while the signed webhook provides server-to-server reconciliation. Both converge on one idempotent fulfillment path.*

![](https://cdn.hashnode.com/uploads/covers/624dca9b375c2653317a1bc3/6a928255-f050-453d-8acf-4864ab8b15d2.png align="center")

*After signature verification, FOSS United created the Late Bird ticket.*

![](https://cdn.hashnode.com/uploads/covers/624dca9b375c2653317a1bc3/74704055-1ce3-4dd2-8095-e6f3718e0d3c.png align="center")

*The regular confirmation email arrived a few minutes later.*

## Was this a security vulnerability?

Not in the usual payment-security sense.

Razorpay Checkout is designed to run in browser code. The merchant's `key_id` and a created `order_id` are therefore visible to the customer. Reusing those public values on another page does not expose FOSS United's private Razorpay secret, change the order amount, redirect the money, or produce a valid payment without actually paying.

The local HTML page was only another container for the same Razorpay Checkout that the official page used. Razorpay still charged the ₹1500 amount associated with FOSS United's order. FOSS United still verified cryptographic proof of the payment before creating the ticket.

It would have been a serious vulnerability if I could send an invented payment ID, forge a webhook, or call a success endpoint without paying and still receive a ticket. That was not possible here.

### The part worth noting

There is still a business-rule edge case here.

FOSS United validates the tier, price, enabled status, expiry date, and inventory when it creates the pending payment record. Razorpay Orders themselves do not automatically expire. An order that was valid when created can therefore remain payable after the ticket tier disappears from the storefront.

Some organizations would accept that as an informal grace period for customers who began checkout before the deadline. Others would not want discounted inventory sold after the advertised cutoff. If FOSS United wants a strict deadline, it should add an application-level expiry to pending payments and re-check that policy before fulfillment, with a defined refund path for a late payment.

I would describe this as an order-lifecycle or business-policy gap, not a severe payment vulnerability. The important security controls still held: the amount was set server-side, the payment was real, both callback and webhook paths required valid Razorpay signatures, and fulfillment was tied to an existing order in FOSS United's database.

## What about overselling?

The same stale-order behavior matters for inventory, not only deadlines.

Imagine a tier with 500 tickets. Customer A starts Checkout while ticket 500 is still available, so FOSS United creates a pending Razorpay order. Customer B completes payment first and receives ticket 500. If Customer A pays the old order afterward, the order amount and signature are still valid, but the tier no longer has capacity.

FOSS United currently checks `maximum_tickets` when it creates the pending payment. After each ticket insert, it counts sold tickets and disables the tier when the limit is reached. Those checks protect the ordinary path, but a pending payment does not reserve inventory. At capture time, ticket creation does not repeat the tier-capacity check before inserting the ticket. Disabling the tier after the count reaches its maximum also does not undo an insertion that has already happened.

There are two related cases:

### A stale pending order after sellout

One order sits unpaid while other buyers consume the remaining inventory. A fulfillment-time validation can catch this: lock the event, count committed tickets, compare the requested quantity with `maximum_tickets`, and avoid issuing tickets if capacity is gone.

### Concurrent captures for the last seat

Two payments can complete at nearly the same time. A simple implementation like this is not sufficient:

```python
if count_existing_tickets() + requested <= maximum_tickets:
    create_tickets()
```

Both transactions might read 499, both decide that one seat remains, and both insert ticket 500. This is a classic time-of-check to time-of-use race.

The check and insertion need serialization. For example, fulfillment can acquire a database row lock on the ticket tier (or its parent event), then count and insert while holding that lock:

With serialization, the second transaction observes the first ticket before deciding. Another valid design is an atomic inventory counter or a short-lived reservation table, but both require more schema and lifecycle management.

The practical rule is: **a Razorpay order reserves a payment amount, not a conference seat**. Inventory reservation belongs in the merchant application.

![Concurrent fulfillment and event row lock diagram](https://cdn.hashnode.com/uploads/covers/624dca9b375c2653317a1bc3/49faf77c-4165-41c1-8065-d1a32b9e026b.png align="center")

*A fulfillment-time capacity check must be serialized; otherwise two captures can both observe the same final seat.*

## Four ways to handle expired or stale orders

### 1\. Rely on Razorpay order expiry

This is not available for Razorpay Orders. Razorpay documents that Orders are immutable and do not automatically expire. Its Checkout `timeout` option only closes a particular modal session; it does not invalidate the order. Payment Links support `expire_by`, but FOSS United uses Orders with embedded Checkout.

This can improve the user experience but cannot enforce a ticket deadline or inventory policy.

### 2\. Reject fulfillment and refund manually

FOSS United could revalidate the tier after payment and refuse to create a ticket if it is expired or full. An operator could later refund the captured payment.

This is small, but it leaves the customer charged with no ticket until somebody handles the case. That is poor failure handling and creates unnecessary support work.

### 3\. Store an application-level `expires_at`

When creating the pending payment, FOSS United could store an immutable `expires_at` value. It could be the tier deadline or a shorter checkout window, depending on policy. Fulfillment would reject and refund payments received after that timestamp.

This makes the policy explicit and avoids relying on a tier record that may be edited later. It requires a schema change, migration, and compatibility behavior for older payment records. It also does not reserve inventory by itself.

### 4\. Revalidate at fulfillment and handle failures manually

Both the browser callback and Razorpay webhook already converge on the same `Razorpay Payment.on_update` hook before ticket creation. That is the smallest central place to revalidate:

*   event ticket sales are still live
    
*   every tier still belongs to the event
    
*   tiers remain enabled and within `valid_till`
    
*   enough inventory remains for the entire payment
    
*   no ticket has already been created for this payment
    

For inventory, this validation must hold a database lock across the capacity check and ticket inserts. If any rule fails, FOSS United should not create tickets. The accepted patch leaves the payment captured, logs the failure, and lets the team handle the case manually under its transfer-only policy.

This fourth approach covers both expired orders and stale orders paid after sellout. With the event lock, it also closes the concurrent-capture race. Without the lock, it only narrows the window and does not guarantee that inventory cannot be oversold.

Before publishing this write-up, I sent the payment-flow fix upstream as [FOSS United pull request #1698](https://github.com/fossunited/fossunited/pull/1698). My first version revalidated fulfillment, serialized capacity allocation, and automatically refunded captured payments that could no longer receive tickets.

The review changed that last part. FOSS United has a transfer-only policy and does not normally issue refunds, so the automatic refund worker and retry scheduler were removed. We also considered a 30-minute `expire_by` value, then dropped it after checking Razorpay's documentation: `expire_by` belongs to Payment Links and is rejected by the Orders API.

The parts that mattered most still merged on August 19:

*   callback, webhook, and manual reconciliation use the same locked capture transition
    
*   fulfillment locks the parent event row before rechecking event status, tier validity, and capacity
    
*   replayed callbacks do not create duplicate tickets
    
*   a late successful capture can recover a locally failed payment
    
*   a failure callback cannot overwrite a payment that was already captured
    
*   unfulfillable captures are logged and left for manual resolution under the transfer policy
    

That is the useful part of open-source review. I proposed one complete flow; the maintainers supplied operational context I did not have; we removed the pieces that did not fit their policy and kept the narrower fix.

A separate security review produced [pull request #1704](https://github.com/fossunited/fossunited/pull/1704), merged on August 21. That PR was not the stale-order fix and it was not my code. It restricted guest ticket responses to an explicit safe field list, reduced data returned by ticket-transfer and tier endpoints, constrained accepted check-in filters, and sanitized IndiaFOSS speaker descriptions before rendering them as HTML.

Both merged changes are now in the FOSS United project and running in production, so it is safe to write about this publicly. Also, none of y'all can try the same thing and cause a headache for the team anymore. Please don't treat that as a challenge.

### How far could this have gone?

My ₹1500 Late Bird order was not even the cheapest version of this. IndiaFOSS also had an ₹800 Enthusiast tier that closed on 30 June, according to the [live stats](https://fossunited.org/indiafoss/2026/stats). So, in theory, somebody could have opened checkout back then, paid nothing, kept the pending Razorpay order around, and completed the ₹800 payment much closer to the event, esentailly hoarding it until you feel comforatable or make plans to attend closer to the event dates.

To be clear, this would not turn a new ₹3000 order into an ₹800 one. The amount was fixed when FOSS United created the order. The trick, if you want to call it that, was simply keeping an old valid order alive after the website stopped selling that tier.

It could get messier with sold-out tickets. The pending order did not reserve a seat, but the old fulfillment path also did not recheck whether the tier had expired, been disabled, or run out of capacity. An old order could therefore have been paid after sellout and still created a ticket. Two payments landing together could even take the number of issued tickets beyond the limit.

The merged fix now locks the event and rechecks the ticket sale, tier, expiry, and available capacity before creating anything. If an old order no longer qualifies, no ticket is issued and the case is logged for the team to handle manually. So yes, no more fun for y'all.

![Recommended payment fulfillment remediation](https://cdn.hashnode.com/uploads/covers/624dca9b375c2653317a1bc3/4cde5d79-46e6-413f-88ce-b81f784947f6.png align="center")

*Both payment-completion paths converge on one serialized validation step, followed by ticket creation or manual handling.*

## What made this possible

Three systems had related but distinct states:

| System | State after midnight |
| --- | --- |
| IndiaFOSS ticket UI | Late Bird was past `valid_till` and could no longer be selected |
| FOSS United payment record | The earlier ₹1500 order was still pending |
| Razorpay | The order remained open and payable |

The ticket-page deadline controlled whether a customer could create a new selection through the UI. It did not invalidate the payment order that had already been created.

Once I paid that order, the rest of the process remained valid:

1.  Razorpay charged the amount already attached to the order.
    
2.  Razorpay returned a signed payment result.
    
3.  FOSS United verified the signature with its private secret.
    
4.  FOSS United marked the pending payment as captured.
    
5.  The stored attendee metadata was used to issue the ticket.
    

This was not a way to change the ticket price or create a ticket without payment. FOSS United's server-side amount calculation prevented the former, and Razorpay's signature verification prevented the latter.

It was an edge case in order expiry: the storefront stopped offering the tier before an already-created payment order stopped being usable.

## Lessons for payment integrations

### Treat the server-calculated amount as authoritative

The client can display a total, but the backend should derive the final amount from trusted products, tiers, discounts, and quantities. FOSS United did this correctly.

### Define what happens to pending orders at a deadline

If a ticket tier expires at midnight, decide whether existing orders receive a grace period. If they should not remain payable, cancel or invalidate them and handle any delayed successful payments safely.

### Verify payment results before fulfillment

A browser saying "payment succeeded" is not enough. Verify Razorpay's signature or consume a trusted webhook before creating tickets or entitlements.

### Make success endpoints idempotent

Payment callbacks and webhooks can be retried. Repeating a valid callback for the same order should confirm the existing purchase, not create duplicate fulfillment.

### Return useful completion data

An HTTP 200 with `{}` was valid but unclear. Returning a status and ticket identifier would make the integration easier to operate:

```json
{
  "status": "captured",
  "ticket_id": "..."
}
```

### Keep callbacks and webhooks conceptually separate

The browser callback improves the immediate user experience. The webhook provides server-to-server reliability if the browser closes or loses connectivity. A robust integration usually accounts for both.

## Where the coding agent helped

The agent was useful for the mechanical parts of the investigation:

*   querying the event API and reading ticket-tier data
    
*   locating the relevant FOSS United source files
    
*   generating the minimal HTML checkout page
    
*   making and inspecting HTTP requests
    
*   opening the success route and confirming the resulting ticket
    

The important reasoning still came from understanding the payment model. I knew why an order ID could outlive the page that created it, why embedding a public key was normal, why the private secret must remain on FOSS United's server, and why the signed result still had to be verified.

That combination worked well: prior experience supplied the hypothesis, open source supplied the exact implementation, and the coding agent reduced the time needed to test it.

## Closing

I started with a simple mistake: after four years of attending IndiaFOSS, I forgot to buy this year's ticket on time. But I learnt an important lesson: do not give up, and keep tinkering around. In the process, I learnt a few things and contributed to open source in its truest fashion.

I have hidden all personal details, PII, and IDs, and used generic screenshots to stop people from recreating this on the platform. I tried to make sure the organizers would not have a bad time dealing with any of that, and I waited until the PRs were merged too. Please be nice to them, okay?

If this was useful and you are interested in open source, payment systems, or the people building both, come to **IndiaFOSS 2026** in Bengaluru. I will be there.

Come say hi.

Anas Khan [anaskhan.me](https://anaskhan.me)
