A proper Stripe integration has three parts: a payment path that fits how you sell (Payment Links, Checkout, Elements or Terminal), a server that creates every payment from prices it controls, and fulfilment triggered by signed, idempotent webhooks such as `checkout.session.completed`, never by the customer reaching your success page. Billing and Connect are layers you add on top.

This guide is the hub for our Stripe engineering articles. It covers how to choose an integration path, Checkout Sessions versus PaymentIntents, what belongs on the server and in the browser, which keys go where, how to fulfil orders from webhooks, idempotency keys, the data model to keep in your own database, testing with sandboxes and test clocks, what people mean by a Stripe connector, a go-live checklist and the mistakes we see most often. Where a topic has its own deep dive, we link to it rather than repeat it. Every API behavior described here was checked against Stripe's documentation in October 2026.

What is a Stripe integration, and which path should you choose?

A Stripe integration is the code and configuration that connects your product to Stripe's APIs so customers can pay and your systems react correctly when they do. Choose the path by asking, in order: is the payment in person, do you need a checkout page at all, and can a Stripe-built page or form fit your flow.

Many teams start from a code sample and only later ask what happens after the payment: who gets access, what ships, what finance reconciles. The path decides how much of that you build. Stripe's own online payments overview lists the options: shareable Payment Links with no code, a prebuilt full page (which Stripe marks as recommended), an embedded form, a custom payment flow with Elements, and the Mobile Payment Element for iOS, Android and React Native apps. In-person payments sit in a separate product, Stripe Terminal.

Billing and Connect are not alternatives to those paths. Billing adds recurring charges, invoices and renewals to whichever path you use. Connect adds other businesses to the money flow: sellers, providers or customers of your software who get paid out. You can run Checkout with Billing, Elements with Connect, or Terminal with Connect. The decision tree below shows the order we ask the questions in.

Illustrative decision path. The highlighted route is a typical online store with its own website, which lands on Checkout Sessions. Billing and Connect sit on top of whichever path you pick.

Payment Links need no code, Checkout gives you a Stripe-built page or form driven by your server, Elements lets you design the payment page yourself, and Terminal handles card-present payments. Each step down that list gives you more control and more code to own.

A Payment Link is a Stripe-hosted payment page you create in the Dashboard or through the API and share as a URL, a button or a QR code. Under the hood it uses Checkout, so Stripe's fulfilment guidance applies unchanged: a Payment Link with nobody listening for webhooks means someone fulfils each order by hand from the Dashboard or email notifications. Fine for a few orders a week, a problem at scale.

Checkout: hosted page or embedded form

With Checkout, your server creates a Checkout Session for each purchase and the customer pays on a Stripe-built UI, either a full page you redirect to or a form embedded in your site. Stripe handles payment method display, validation, authentication, and optional tax, discounts, shipping and address collection. Your server still decides what is bought and for how much, and still owns fulfilment. For most online businesses this is the right default.

Elements and the Payment Element: your own checkout page

Elements are UI components you place inside your own page. The Payment Element is a single component that renders the payment methods you have enabled. Choose it when the payment step must live inside a flow you control, such as an onboarding wizard or a booking flow with live availability. You then own layout, error states and the confirmation step. Elements can sit on top of Checkout Sessions or PaymentIntents, which is the decision in the next section.

Terminal: in-person payments

Stripe Terminal takes card-present payments through supported readers or, on compatible phones and tablets, Tap to Pay. The server side looks familiar: you still create a PaymentIntent on your server and still rely on webhooks to confirm the outcome. The device side adds reader discovery, connection tokens, cancellation and timeout handling. Our guide to Stripe Terminal and Tap to Pay in Flutter and Android kiosks covers that architecture in detail, and our Stripe Terminal service describes how we build it.

Billing: adding subscriptions and invoices

A Stripe Billing integration adds Products, Prices, Subscriptions and Invoices. The payment page can still be Checkout (with `mode: "subscription"`) or Elements. What changes is the lifecycle: renewals, failed payments, retries, upgrades, cancellations and trials all happen later, without the customer present, and arrive at your server only as webhook events. That is why Billing work is mostly event handling and access rules. We cover it in three dedicated guides: which subscription webhook events to handle and when to grant access, failed payments, retries and dunning, and migrating subscriptions to Stripe Billing. Our Stripe Billing service builds and repairs these lifecycles.

Connect: when other businesses get paid

Connect is for marketplaces and platforms: whenever money you collect is partly or wholly owed to someone else. It adds connected accounts, onboarding, charge types and payouts, and each of those carries responsibility for refunds, disputes and negative balances. Two decisions dominate: the charge type, covered in Stripe Connect direct charges vs destination charges, and the seller onboarding flow, covered in Stripe Connect onboarding for marketplaces. Make both before writing payment code. Our Stripe Connect service starts there.

Checkout Sessions vs PaymentIntents: which API should you build on?

Stripe recommends the Checkout Sessions API for most integrations, including ones that use Elements for a custom UI. Choose the lower-level PaymentIntents API only when you want to own every part of checkout yourself, including tax, discounts, shipping, subscriptions and currency conversion.

A PaymentIntent tracks one attempt to collect one amount of money through its lifecycle: created, waiting for a payment method, possibly waiting for customer authentication, processing, succeeded or canceled. A Checkout Session sits one level higher. It represents the whole purchase: line items, quantities, customer details, tax, discounts, shipping and, for subscriptions, the subscription it creates. After a successful payment, the session references the customer and either the PaymentIntent or the Subscription.

Stripe's comparison on the online payments page is direct about the trade-off. With PaymentIntents you calculate the final amount and manage the entire checkout lifecycle in your own code, and you integrate tax, subscriptions and discounts separately. Checkout Sessions include those features, expire automatically after 24 hours and emit webhook events for the complete checkout lifecycle, while PaymentIntents emit payment status events only and never expire on their own.

  • Choose Checkout Sessions when you sell catalog items, bookings, credits or subscriptions; when you want Stripe Tax, coupons or shipping rates without rebuilding them; or when you want one object to fulfil from.
  • Choose PaymentIntents when the amount is computed by a pricing engine you already own and must stay authoritative, when you need fine control over capture timing (for example, authorize now and capture after a manual review), or when you are building in-person payments with Terminal.
  • Avoid mixing without a reason. Running both for the same kind of purchase doubles your fulfilment paths and your test matrix. If you must, store which API each order used so refunds and reconciliation follow the right object.

With Checkout Sessions you fulfil the session; with PaymentIntents you usually fulfil from `payment_intent.succeeded`. The principles below apply to both.

What runs on the server, what runs in the browser, and which keys go where?

The browser only collects payment details and shows results, using a publishable key. Your server decides what is being bought and for how much, creates the Checkout Session or PaymentIntent with a restricted or secret key, and receives webhooks. Nothing the browser sends should ever set a price.

This split is the single most important security property of a Stripe integration. The browser says which product and quantity the customer chose; the server looks up the Price in its own configuration and creates the session. Here is a minimal hosted Checkout Session in Node.js with Express. Item and price lookups come from your own database, and the order is created before the session so every session belongs to exactly one order.

Creating a Checkout Session on the server (Node.js, Express)
import express from "express";
import Stripe from "stripe";

// A restricted key with only the permissions this service needs.
// Pin the API version you tested against (see the go-live checklist).
const stripe = new Stripe(process.env.STRIPE_API_KEY);
const app = express();

app.post("/checkout", express.json(), async (req, res) => {
  const user = await requireUser(req); // your auth
  // The browser sends SKUs and quantities only. Prices come from your catalog.
  const order = await db.orders.createPending(user.id, req.body.items);

  const session = await stripe.checkout.sessions.create(
    {
      mode: "payment",
      line_items: order.items.map((item) => ({
        price: item.stripePriceId, // looked up on the server
        quantity: item.quantity,
      })),
      ...(user.stripeCustomerId ? { customer: user.stripeCustomerId } : {}),
      client_reference_id: order.id,
      metadata: { order_id: order.id },
      success_url: "https://example.com/orders/complete?session_id={CHECKOUT_SESSION_ID}",
      cancel_url: "https://example.com/cart",
    },
    { idempotencyKey: "checkout-session-" + order.id },
  );

  await db.orders.attachSession(order.id, session.id);
  res.json({ url: session.url }); // the browser redirects here
});

Three details in that snippet do real work. The `{CHECKOUT_SESSION_ID}` placeholder in `success_url` is replaced by Stripe with the session ID, which lets your landing page look the order up. The `metadata.order_id` and `client_reference_id` tie the Stripe object back to your order without relying on email addresses. And the idempotency key means a retried request after a network timeout returns the same session instead of creating a second one.

Publishable, restricted and secret keys

Stripe's API keys documentation describes three key types for a single account. Publishable keys (`pk_test_` and `pk_live_`) are safe to ship in front-end code: they identify your account and can create tokens or PaymentMethods, but cannot create charges or read account data. Secret keys (`sk_`) have unrestricted access to every API. Restricted keys (`rk_`) carry only the permissions you grant. Stripe now recommends restricted keys for most use cases, does not recommend secret keys for new use cases, and suggests migrating existing secret key usage to restricted keys.

  • One restricted key per service. The checkout service needs to write Checkout Sessions and read Prices. A reporting job needs read access. Neither needs to create payouts or change account settings.
  • Keys live in a secrets vault, or environment variables where no vault exists, never in source control or configuration files checked into the repository.
  • Live keys you create are shown once. Stripe can reveal only live keys it created for you; a key you created yourself cannot be revealed again, so store it before closing the dialog.
  • Rotation has a grace period. When you rotate a key in the Dashboard, the old and new keys both work for up to 7 days, so you can roll out the new key gradually and expire the old one once its request logs show no traffic.
  • Access policies can limit a key to known IP addresses or, for dynamically scaled infrastructure, to specific networks and countries. Stripe recommends configuring them on all live keys.
  • Webhook signing secrets are not API keys. Each endpoint has its own `whsec_` secret, and when you move to live mode you register the live endpoint and deploy its new secret.

Which webhook event should trigger shipping the order or unlocking access after a hosted Checkout payment?

Fulfil from `checkout.session.completed` when the session's `payment_status` is not `unpaid`, and from `checkout.session.async_payment_succeeded` for delayed payment methods such as ACH direct debit. Never rely on the customer reaching your success page: they can pay and lose their connection before it loads.

Stripe's Checkout fulfilment guide is unusually explicit here. Webhooks are required for fulfilment, because it is not guaranteed that customers visit your landing page. They are also required if you sell subscriptions or accept payment methods with delayed success notification, because those change state after the Checkout Session completes. The landing page is still useful, but as a fast path, not the trigger.

The pattern Stripe recommends is a single fulfilment function that both the webhook handler and the landing page call with the Checkout Session ID. It must handle being called more than once, possibly concurrently, for the same session. It retrieves the session from the API with `line_items` expanded, checks `payment_status`, fulfils the line items and records that fulfilment happened.

  • `payment_status: "paid"`: the payment succeeded. Fulfil.
  • `payment_status: "no_payment_required"`: nothing to charge, for example a fully discounted order or a free trial. Stripe's sample code fulfils anything that is not `unpaid`, so decide deliberately whether these orders grant access.
  • `payment_status: "unpaid"`: for a delayed method the payment is still processing. Do not ship. Wait for `checkout.session.async_payment_succeeded`, and listen for `checkout.session.async_payment_failed` so you can tell the customer.
Fulfilment hangs off the signed webhook, not the redirect. The success page may call the same idempotent function to show results sooner, but the order must complete even if the page never loads.

One timing detail is worth knowing. When you have a webhook endpoint listening for `checkout.session.completed` and you set a `success_url`, hosted Checkout waits up to 10 seconds for your server to respond to the event before redirecting the customer. A slow handler therefore delays every customer's redirect. Stripe's general webhook guidance is the same: return a 2xx response quickly, before any complex logic, and do the work in a queue. When you test locally with the Stripe CLI, Checkout redirects immediately, so this behavior only shows up against a deployed endpoint.

Webhook endpoint for Checkout events (Node.js, Express)
// Register this route before any global express.json() middleware:
// signature verification needs the raw, unparsed request body.
app.post(
  "/webhooks/stripe",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    let event;
    try {
      event = stripe.webhooks.constructEvent(
        req.body,
        req.headers["stripe-signature"],
        process.env.STRIPE_WEBHOOK_SECRET,
      );
    } catch {
      return res.status(400).send("Invalid signature");
    }

    // Record the event and enqueue its job in one transaction.
    // A unique index on event_id turns a duplicate delivery into a no-op.
    await db.transaction(async (tx) => {
      const isNew = await tx.stripeEvents.insertIfAbsent(event.id, event.type);
      if (!isNew) return;
      const sessionId = event.data.object.id;
      switch (event.type) {
        case "checkout.session.completed":
        case "checkout.session.async_payment_succeeded":
          await tx.jobs.enqueue("fulfill-checkout", { sessionId });
          break;
        case "checkout.session.async_payment_failed":
          await tx.jobs.enqueue("notify-payment-failed", { sessionId });
          break;
      }
    });

    res.sendStatus(200); // fast 2xx; the worker does the slow part
  },
);
An idempotent fulfilment function (Node.js)
// Called by the queue worker and by the success page. Safe to run
// many times, even concurrently, for the same Checkout Session.
export async function fulfillCheckout(sessionId) {
  const session = await stripe.checkout.sessions.retrieve(sessionId, {
    expand: ["line_items"],
  });

  // Delayed methods stay "unpaid" until async_payment_succeeded arrives.
  if (session.payment_status === "unpaid") return { fulfilled: false };

  const order = await db.orders.findBySession(session.id);
  if (!order) throw new Error("No order for session " + session.id);
  if (session.amount_total !== order.amountExpected) {
    await alerts.raise("amount_mismatch", { orderId: order.id });
  }

  // Atomic claim: UPDATE ... SET status = 'fulfilling'
  // WHERE id = ? AND status = 'pending'. Only one caller wins.
  const claimed = await db.orders.claimForFulfilment(order.id);
  if (!claimed) return { fulfilled: false }; // done or in progress

  await shipOrGrantAccess(order, session.line_items.data);
  await db.orders.markFulfilled(order.id, {
    paymentIntentId: session.payment_intent,
  });
  return { fulfilled: true };
}

The atomic claim is what makes concurrent calls safe: the webhook worker and the success page can both arrive within milliseconds, and only one wins the update. The remaining risk is a crash between the claim and `markFulfilled`. Handle it with a sweep that finds orders stuck in `fulfilling` and retries `shipOrGrantAccess`, which therefore needs to be idempotent too (for example, granting access is a set operation, and a shipping request carries the order ID as its own idempotency key). If a session has many line items, use auto-pagination on the line items endpoint rather than relying on the expanded first page.

Signature verification, duplicate deliveries, out-of-order events, retries and reconciliation jobs are covered in depth in our guide to Stripe webhooks best practices. The short version: verify every event with the raw body, deduplicate by event ID, never assume order, and run a reconciliation job that compares Stripe with your database after any outage.

Idempotency keys: how do you stop a retry from charging twice?

Send an `Idempotency-Key` with every POST request that creates or changes something. If the connection drops and you retry with the same key, Stripe returns the saved result of the first request instead of creating a second session, refund or transfer.

Stripe's idempotent requests reference sets out the rules for API v1. All POST requests accept idempotency keys; sending one with GET or DELETE has no effect because those are idempotent by definition. Keys are up to 255 characters. Stripe saves the status code and body of the first request once the endpoint begins executing, and retries with the same key return that saved response, including 500 errors. Keys can be pruned once they are at least 24 hours old, after which a reused key creates a new request. The idempotency layer also compares parameters, and a retry with the same key but different parameters returns an error.

Those rules shape how you generate keys:

  • Derive keys from the business operation, not the HTTP attempt. `checkout-session-` plus your order ID, or `refund-` plus your refund request ID, means any retry of that operation maps to the same key. Stripe suggests V4 UUIDs or other high-entropy strings; a UUID you generate once and store on the order row works well.
  • Never put personal data in a key. Stripe specifically advises against email addresses and other personal identifiers.
  • A changed request needs a new key. If the customer edits their cart, the parameters differ and the old key will be rejected. Create a new order version, or a new key with a version suffix.
  • A saved 500 stays saved. Because the error is replayed for that key, a request that failed with a server error needs a deliberate decision: check whether the object was created, then retry with a new key if it was not.
  • Validation errors are not saved. If parameters fail validation, or the request conflicts with another one executing concurrently, no result is stored and you can retry with the same key.

Idempotency keys protect outgoing requests; webhook deduplication protects incoming events. You need both.

What data should a Stripe integration keep in your own database?

Keep the Stripe IDs that link your records to Stripe objects, the amounts you expected, your own fulfilment state and a log of processed event IDs. Do not copy card data or mirror every Stripe field; Stripe remains the source of truth for payment state.

The four tables below cover most one-time and subscription integrations. Field names are illustrative.

An illustrative minimum. Unique constraints on Stripe IDs and event IDs do most of the work of preventing duplicates.
  • Customers. Map each user to one Stripe Customer, stored with a unique constraint, and reuse it.
  • Orders. Create the order before the payment, store the Checkout Session ID, then the PaymentIntent ID. Keep the expected amount to compare with `amount_total`, and `livemode` so test data never reaches reports.
  • Events. Each processed event ID with a unique index, timestamps and any error: your deduplication guard and audit log.
  • Subscriptions. Status, price and period end, with access derived from status by your own policy rather than from whichever event arrived last.
  • Not card data. With Checkout or Elements, card numbers never touch your server; keep them out of your logs too.

How do you test a Stripe integration: sandboxes, the CLI and test clocks?

Build in a sandbox with test keys, forward events to your laptop with `stripe listen`, pay with test cards, and use test clocks to move subscriptions through time. Test failure paths, not just the happy path: declines, authentication, duplicates, delays and out-of-order events.

Sandboxes and live mode are isolated: each has its own keys, and objects created in one cannot be used in the other. Stripe recommends a separate general sandbox for new integrations, and its Billing testing guide goes further, suggesting separate sandboxes for local development and continuous integration so automated tests do not disturb your products, prices, customers or webhook endpoints.

Local webhooks with the Stripe CLI

Run `stripe listen --forward-to localhost:4242/webhooks/stripe`, copy the signing secret it prints into your local environment, and complete a real test checkout with the card `4242 4242 4242 4242`, any future expiry date and any CVC. You should see `checkout.session.completed` forwarded and your fulfilment function run once. Then resend the event and confirm nothing is fulfilled twice. `stripe trigger` is useful for smoke tests, but events created that way carry fixture data that does not match your orders; Stripe's own guidance is that real test objects are the most reliable way to test webhook handling.

Test clocks for subscriptions

Test clocks simulate the passage of time in a sandbox, so you can see an annual renewal fail without waiting a year. You can run them from the Dashboard as simulations or through the API. In the Dashboard, a simulation advances by up to two subscription intervals at a time, and finishing a simulation deletes the objects it created, including the customer and subscription. Use them to test trials (the `customer.subscription.trial_will_end` event arrives three days before a trial ends), renewals, upgrades mid-cycle and failed renewals with the test card that attaches successfully but declines on charge. Our subscription webhooks guide lists the transitions worth running.

A failure matrix worth running

  • A paid order, fulfilled once even when the event is resent.
  • A declined card, and a payment that requires authentication.
  • A delayed payment method that succeeds later, and one that fails.
  • The webhook arriving with the success page never loaded.
  • Your endpoint returning 500 for an hour, then recovering.

What is a Stripe connector, and when is a connector enough?

A "Stripe connector" usually means a prebuilt integration rather than your own code: a plugin for an e-commerce or website platform, a no-code automation step that reacts to Stripe events, or a data connector that copies Stripe records into a warehouse, CRM or accounting system. Connectors are enough when nothing critical depends on exactly-once behavior.

Searches for a Stripe connector usually come from one of three needs.

  • Taking payments on a platform you already use. If your store or booking system offers a Stripe plugin, use it. The plugin owner handles the integration, and most of Stripe's go-live checklist does not apply to you, as Stripe itself notes for connected websites and plug-ins.
  • Automating follow-up work. No-code automation tools can send a Slack message on a new payment, add a customer to a CRM or create a row in a spreadsheet. That is reasonable for notifications and internal workflows.
  • Moving data for reporting. Data connectors sync Stripe objects into analytics or accounting tools. They are read-only and low-risk, provided the connector uses a restricted key with read permissions only.

A connector stops being enough when the action it triggers must happen exactly once, in the right order, with retries you can see. Granting paid access, shipping goods, moving money to sellers, issuing refunds, and keeping subscription access in step with billing state all fall into that category. Most automation steps act on one event at a time, have limited deduplication and retry visibility, and run on a key you may not have scoped. If a missed or doubled event costs you money or a customer, that logic belongs in your own code, built to the patterns in this guide.

Stripe integration go-live checklist and common mistakes

Before switching to live keys, pin the API version, register the live webhook endpoint, move live restricted keys into a vault, confirm your handlers survive duplicate, delayed and out-of-order events, review error handling and logs, and replace every test object ID.

Stripe publishes a go-live checklist for developers. Its items, with what they mean in practice:

  • Set the API version. Requests use your account's API version unless overridden, and webhook events are shaped by the account version unless you set one on the endpoint. In Node.js and other dynamic languages, set the version in the library so a Dashboard upgrade does not change your payloads underneath you.
  • Handle edge cases. Test with incomplete, invalid and duplicate data, including retrying the same request.
  • Review API error handling. A declined card (`card_error`) is a message for the customer. An `invalid_request_error` is a bug in your code and should page someone, not show a raw message.
  • Review logging. Your own logs are the backup when your server cannot reach Stripe. Keep nothing sensitive in them.
  • Do not rely on test objects. Sandbox products, prices and coupons do not exist in live mode; reference live ones from per-environment configuration.
  • Register production webhooks. Define live endpoints and confirm they behave exactly like test ones, handling delayed and duplicate notifications without depending on event order.
  • Change and secure your API keys. Rotate keys that may have been saved outside your codebase during development, and make sure no key is in the code.
A condensed version of the checklist we run before the first live charge, combining Stripe's go-live items with the fulfilment and idempotency rules in this guide.

Common Stripe integration mistakes

  • Fulfilling on the success page only. Customers who close the tab after paying never get their order. Stripe states plainly that webhooks are required.
  • Trusting amounts from the browser. Prices come from your server's catalog, never from the request body.
  • Parsing the webhook body before verifying it. A global JSON parser changes the raw body, verification fails, and teams switch it off. Mount the raw parser on the webhook route instead.
  • Treating `checkout.session.completed` as paid. For delayed methods, `payment_status` is still `unpaid`.
  • One unrestricted secret key everywhere. The same `sk_live_` key in the web app, cron jobs and a third-party automation multiplies the damage of one leak.
  • Creating a new Customer on every purchase. Saved payment methods and history fragment across duplicates.
  • Unpinned API versions. An account-level upgrade silently changes webhook payload shapes.

When should you hire Stripe developers?

Hire Stripe developers when the integration carries risk your team has not handled before: subscriptions with access rules, Connect money flows, in-person payments, a migration from another provider, or an existing integration that double-fulfils, misses events or cannot be reconciled. A Payment Link or a platform plugin rarely needs outside help.

The signals usually show up in support tickets before code: customers who paid but did not get access, refunds issued twice, subscriptions canceled in Stripe but active in your app. When you evaluate a developer or a firm, ask questions that reveal whether they have run production payments:

  • Which event do you fulfil from, and what if the customer never reaches the success page?
  • How is the webhook handler safe against duplicates and out-of-order delivery?
  • Where do idempotency keys come from, and what do you do after a 500 response?
  • Which keys will you need, with which permissions, and how will you receive them?
  • Will the code live in our repository and Stripe account, and what do we get at handover?

Anyone who asks you to email a live secret key, or who cannot answer the first question in one sentence, is a risk. For a fuller comparison of hiring models, see dedicated developers vs freelancers vs agencies, and for what a Stripe partner engagement involves, what it means that Cevrynt is a Stripe partner. Our page on how to hire Stripe developers sets out how we staff this work.

From here, this article is about Cevrynt

How Cevrynt's engineers run a Stripe integration

Cevrynt, Inc. is a Stripe partner in the Stripe Partner Ecosystem. The partnership belongs to the company; the certifications belong to the people who hold them. Arindam Pradhan, our founder and engineering lead, is a Stripe Certified Professional (Implementation Architect). Anupam Pradhan and Subhadip Giri, a full stack developer, are Stripe Certified Professionals (Payments Architect). The people who scope your Stripe work are the people who build it.

We work the way this guide reads: we choose the path and write down the events, records and access rules first, then build in your repository and your Stripe account with least-privilege access, and we never ask for raw production API secrets by email. Before go-live we run the failure paths in Stripe test mode, including duplicated and out-of-order events, then release with a rollback path and hand over monitoring notes. For in-person payments, we built a Stripe Terminal Tap to Pay kiosk for Megapos Ltd, described on our Stripe Terminal page.

Every Stripe project is scoped after a technical review. Start with the Stripe services overview, go straight to Billing or Connect, or tell us about your integration and we will reply with the questions we need answered first.