Skip to main content
友田 陽大
Payments & billing
Stripe
決済
Next.js
TypeScript
Webhook
冪等性
セキュリティ
B2B SaaS
アーキテクチャ設計

The Complete Guide to Implementing Stripe Payments at Production Quality (2026 Edition, Official-Documentation-Conformant): Checkout Sessions, Webhooks, Idempotency, and Connect in Real Code

An implementation guide to production payments faithful to the Stripe official documentation. We implement hosted/embedded payments on Next.js 16 with the 2026-standard Checkout Sessions API, process Webhooks idempotently as the single source of truth, prevent double charges with Idempotency-Key and event.id, and handle marketplaces with Connect. With practical knowledge from 0 double charges in production.

Published
Reading time
20 min read
Author
友田 陽大
Share

Payments are the domain where the gap between "it worked" and "it's shippable to production" is the farthest in software. A demo where you enter a card number and a success screen appears can be built in a day. But not emitting a single double charge, integrity not breaking even when the network is cut, the amount being protected even when the web request is tampered with — only after doing this far do you have the qualification to hold your customers' money.

This article is an implementation guide that's faithful to the latest specs of the Stripe official documentation, yet written so you understand "in which situation, why, and how to use it" better than the official. The latest API version at the time of writing is 2026-05-27.dahlia. The samples are shown in my main battlefield, Next.js 16 (App Router / RSC) + TypeScript strict.

About this article's credibility: I designed and led the payment-reliability layer (idempotency, atomic balance updates, zero-downtime migration) on an environment-sector serverless payment platform, and achieved 0 double charges in production (I handled about 60% of the repository's commits = 403 of 694). I also operate Stripe Connect in production on a lumber-distribution B2B SaaS (METI Minister's Award), and Stripe 17's idempotent Webhook processing on a financial-education subscription platform. All the Stripe specs in the text are backed by the official documentation, and I don't deal with unverified numbers like ROI.


Below, I dig into each in real code.


1. The 2026 map: which integration method to choose

Because Stripe's integration methods have historically increased, older articles are often written on the somewhat-dated premise of "create a PaymentIntent and pass the clientSecret to Elements." The official's recommendation in 2026 has clearly changed.

The official expression: "For most implementations, we recommend the Checkout Sessions API. Choose Payment Intents only when you manage the entire payment flow yourself and rebuild features like tax, discounts, subscriptions, and currency conversion on your own."

In other words, the decision axis is the trade-off between "freedom of customization" and "the amount of implementation/maintenance you shoulder yourself."

ViewpointHosted CheckoutEmbedded CheckoutElements + Payment Intents
Implementation amountMinimalSmallLarge (rebuild much yourself)
UI freedomLow (Stripe page)Medium (embed a frame in your site)Maximum (fully custom)
Tax/discount/shipping/address collectionBuilt-inBuilt-inManual implementation
Subscription creationBuilt-inBuilt-inImplemented separately
Auto session expiryYes (24 hours)YesNo
Webhook eventsThe whole payment lifecycleWholePayment status only
PCI burden (self-questionnaire)SAQ A (minimal)SAQ ASAQ A (needs an implementation meeting the requirements)
Suited forBuild and sell first / MVP / billingEC wanting to keep the brand experienceWhere a unique payment UX is the core of the business

The decision shortcut:

  • "I want to start payments fast and safely, no matter what" → hosted Checkout.
  • "I don't want to make them leave my site (want to maintain the brand experience)" → embedded Checkout.
  • "The payment form itself is the product's differentiator, and I'm prepared to do state management myself" → Elements + Payment Intents.

In my practice too, 90% of projects were fine with hosted or embedded. A raw Payment Intents implementation is needed only for cases where the UX is truly the core, like bundling multiple payments in one screen or interposing a custom credit flow. It's a typical branch point where YAGNI (don't build until needed) takes effect.


2. Setup: a type-safe Stripe client and the boundary of secrets

First, the shared client. Always pin the API version. Don't pin it, and a Stripe-side default update changes the response shape, inviting a non-reproducible failure where it suddenly breaks one day.

// lib/stripe.ts — サーバー専用。"use client" のファイルから絶対に import しない。
import Stripe from "stripe";

const secretKey = process.env.STRIPE_SECRET_KEY;
if (!secretKey) {
  // 起動時に落とす。秘密鍵欠落のまま本番に出さない(フェイルファスト)。
  throw new Error("STRIPE_SECRET_KEY is not set");
}

export const stripe = new Stripe(secretKey, {
  // 執筆時点の最新。アップグレードは差分を確認してから意図的に上げる。
  apiVersion: "2026-05-27.dahlia",
  appInfo: { name: "your-app", version: "1.0.0" }, // サポート時の調査が速くなる
  typescript: true,
});

You won't have accidents if you guard just 3 security boundaries.

  • STRIPE_SECRET_KEY (sk_...) is server-only. Inject it via environment variables, and don't put it in logs or the client bundle.
  • Only the publishable key (pk_...) may go to the browser. In Next.js, NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY.
  • The Webhook signing secret (whsec_...) is unique per endpoint. This too via environment variables.

Next.js pitfall: importing lib/stripe.ts from a Client Component can mix the secret key into the bundle. The iron rule is to limit touching Stripe to Route Handlers, Server Actions, and Server Components, and to pass the client only throwaway values like clientSecret or session.url.


3. Hosted Checkout: shippable payments with minimal code

The shortest production path. Create a Checkout Session on the server and redirect to Stripe's hosted page. Written with Next.js 16's Server Action, you can pass it directly to a form's action, which is clean.

// app/checkout/actions.ts
"use server";

import { redirect } from "next/navigation";
import { headers } from "next/headers";
import { stripe } from "@/lib/stripe";

export async function createHostedCheckout(priceId: string): Promise<never> {
  const origin = (await headers()).get("origin") ?? "https://example.com";

  const session = await stripe.checkout.sessions.create(
    {
      mode: "payment", // 単発決済。サブスクなら "subscription"
      line_items: [
        // ❶ 金額はクライアントから受け取らない。サーバーが知る Price ID で確定する。
        { price: priceId, quantity: 1 },
      ],
      automatic_tax: { enabled: true }, // 税計算は Stripe に任せる(組み込み)
      success_url: `${origin}/checkout/success?session_id={CHECKOUT_SESSION_ID}`,
      cancel_url: `${origin}/checkout/cancel`,
    },
    // ❷ 冪等キー。二重サブミットでも Checkout Session を二重に作らせない(§6で詳述)
    { idempotencyKey: `checkout:${priceId}:${crypto.randomUUID()}` },
  );

  if (!session.url) throw new Error("Checkout Session has no URL");
  redirect(session.url); // 303 相当でリダイレクト
}

The most important thing here is ❶. You must not pass a raw amount (price_data.unit_amount) to line_items, and certainly not a client-derived value. Since browser-side JS is tamperable, if amount: 100 can be rewritten to amount: 1, it's over. Reference a Price ID (price_...) made in the Stripe dashboard, or, if you absolutely need dynamic pricing, assemble unit_amount only from a server-side DB / computation result. This is the payment version of OWASP's "don't trust the client."

The client can be a normal form, with even JavaScript minimal.

// app/checkout/buy-button.tsx — Server Component のままでよい
import { createHostedCheckout } from "./actions";

export function BuyButton({ priceId }: { priceId: string }) {
  return (
    <form action={createHostedCheckout.bind(null, priceId)}>
      <button type="submit" className="btn-primary">
        購入する
      </button>
    </form>
  );
}

The point that you must not make success_url the grounds for "payment success" is described in detail in §5. Redirects are for the user's experience, and confirmation processing is for the Webhook — separate the roles.


4. Embedded Checkout: don't make them leave your site

For EC and SaaS wanting to keep the brand experience, embed the payment form in your own site without flying off to Stripe's page. On the server side, specify ui_mode: 'embedded_page' and return the client_secret to the client. The redirect destination becomes return_url, not cancel_url.

// app/api/checkout-session/route.ts
import { NextResponse } from "next/server";
import { headers } from "next/headers";
import { stripe } from "@/lib/stripe";

export async function POST(): Promise<NextResponse> {
  const origin = (await headers()).get("origin") ?? "https://example.com";

  const session = await stripe.checkout.sessions.create({
    ui_mode: "embedded_page", // ← 埋め込みモード
    mode: "payment",
    line_items: [{ price: process.env.PRICE_ID!, quantity: 1 }],
    automatic_tax: { enabled: true },
    // 完了後の戻り先。{CHECKOUT_SESSION_ID} は Stripe が実IDに展開する
    return_url: `${origin}/checkout/return?session_id={CHECKOUT_SESSION_ID}`,
  });

  return NextResponse.json({ clientSecret: session.client_secret });
}

The client uses the official React components EmbeddedCheckoutProvider and EmbeddedCheckout. Just pass a function that goes to fetch the clientSecret, and Stripe takes care of everything from form rendering, validation, to 3-D Secure (SCA).

// app/checkout/embedded.tsx
"use client";

import { useCallback } from "react";
import { loadStripe } from "@stripe/stripe-js";
import {
  EmbeddedCheckoutProvider,
  EmbeddedCheckout,
} from "@stripe/react-stripe-js";

// モジュールトップで一度だけ。再レンダリングのたびに loadStripe しない。
const stripePromise = loadStripe(
  process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!,
);

export function EmbeddedCheckoutForm() {
  const fetchClientSecret = useCallback(
    () =>
      fetch("/api/checkout-session", { method: "POST" })
        .then((res) => res.json())
        .then((data: { clientSecret: string }) => data.clientSecret),
    [],
  );

  return (
    <div id="checkout" aria-live="polite">
      <EmbeddedCheckoutProvider stripe={stripePromise} options={{ fetchClientSecret }}>
        <EmbeddedCheckout />
      </EmbeddedCheckoutProvider>
    </div>
  );
}

a11y note: the accessibility of the embedded form itself is guaranteed by the Stripe side, but the loading state while loading and the message on error are your own UI. Have state changes read aloud with aria-live="polite", and while the payment button is disabled, always add aria-disabled and visual feedback. An a11y gap on the payment screen produces the worst abandonment of "I put it in the cart but can't buy."


5. The source of truth is the Webhook: don't confirm the order on the success screen

This is the biggest watershed separating a beginner from someone with production experience. Reaching success_url/return_url does not mean "payment success."

There are 3 reasons. ① The user may close the tab after payment and the redirect won't complete. ② URLs are guessable/tamperable (?session_id=... can be hit on their own). ③ For payment methods that confirm later, like bank transfer or convenience-store payment, the deposit is incomplete at redirect time.

Therefore, always do "confirmation processing" like inventory allocation, license issuance, email sending, and balance addition in the Webhook. Stripe keeps re-sending an event "with exponential backoff for up to 3 days," so even if your server goes down for a moment, you won't miss it.

This chapter narrows to the overall picture. The technique of Webhook signature verification, resilience to "at-least-once / out-of-order" delivery, and designing the subscription lifecycle as a state machine is dug into at the real-code level in the dedicated article Implementing Stripe Webhooks and Idempotency at Production Quality. The division of roles is: the map of the whole payment is this article, the build-out of Webhooks is the dedicated article.

5.1 Signature verification: constructEvent against the raw body

What Stripe officially says repeatedly is "do signature verification with the raw request body." If the framework auto-parses the JSON, the signature stops matching. Next.js 16's App Router can get the raw body by calling await req.text() in a Route Handler, so the Pages-Router-era bodyParser: false setting is unnecessary.

// app/api/webhooks/stripe/route.ts
import { NextResponse } from "next/server";
import type Stripe from "stripe";
import { stripe } from "@/lib/stripe";
import { handleStripeEvent } from "@/lib/stripe/handle-event";

export const runtime = "nodejs"; // 署名検証に Node の crypto を使う

const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;

export async function POST(req: Request): Promise<NextResponse> {
  const body = await req.text(); // ← 生ボディ。JSON.parse しない
  const signature = req.headers.get("stripe-signature");
  if (!signature) {
    return NextResponse.json({ error: "missing signature" }, { status: 400 });
  }

  let event: Stripe.Event;
  try {
    // constructEventAsync はどの実行環境でも安全に動く(推奨)。
    event = await stripe.webhooks.constructEventAsync(
      body,
      signature,
      webhookSecret,
    );
  } catch (err) {
    // 署名不一致・タイムスタンプ超過(既定5分)はここで弾く=リプレイ攻撃対策
    const message = err instanceof Error ? err.message : "invalid signature";
    return NextResponse.json({ error: message }, { status: 400 });
  }

  try {
    await handleStripeEvent(event); // ❸ 重い処理はここ。ただし速く返す工夫が要る
  } catch (err) {
    // 5xx を返すと Stripe が再送してくれる(=後述の冪等処理が効く)
    console.error(`Webhook handler failed for ${event.id}`, err);
    return NextResponse.json({ error: "handler failed" }, { status: 500 });
  }

  return NextResponse.json({ received: true });
}

Note that signature verification simultaneously becomes a replay-attack countermeasure. The Stripe-Signature header contains a timestamp (t=), and by default an old signature exceeding the 5-minute allowance is rejected. Don't set the allowance to 0 (a slight clock drift would drop even legitimate events) — that too is an official caution. Make the Webhook route exempt from CSRF protection, and make signature verification the sole authentication grounds.

5.2 Achieve both "return 2xx fast" and "heavy processing"

The official says "to avoid timeouts, return 2xx quickly before complex logic." On the other hand, confirmation processing is heavy. The straight-up way to handle this contradiction is to split into "commit only the fact of receipt immediately → the actual processing is async." For small scale, synchronous processing is in time, but if email sending or external-API integration is involved, the standard play is to save just the receipt record first and flow it to a job queue (Vercel Queues or a DB-based outbox).

With this, we're ready to face the Webhook reality that "delivery is at least once (with duplicates) / out-of-order is possible." The next chapter is its core.


6. Eradicate double charges by structure: the 2-layer model of idempotency

What you absolutely want to avoid in payments are double charges and state rollback. The reason I could achieve 0 double charges in production on a payment platform is not grit or monitoring, but that I embedded idempotency into the structure of the code. In Stripe, you make it take effect at 2 layers.

6.1 Sending side: uniquify the request with Idempotency-Key

It's a normal case for the same payment request to reach Stripe multiple times due to a network timeout or serverless retry. Attach an Idempotency-Key, and Stripe saves the first request's result (status code and body) and returns exactly the same result for a re-send with the same key — it doesn't create a new charge. Let me summarize the official spec precisely.

  • The target is all POST requests. GET/DELETE are idempotent by definition, so unneeded.
  • The key format is "a V4 UUID, or a random string with enough entropy to avoid collisions," at most 255 characters.
  • Don't make an email address or personal identifier the key (leakage/guessing risk).
  • The retention period is at least 24 hours. Reusing it after the key is pruned past that is treated as a new request.
  • Sending different parameters with the same key as the first request errors (misuse prevention).
  • The result is saved regardless of success or failure, and even a 500 error is returned as-is.
// 業務的に意味のあるキーを使うと、リトライがちゃんと同一視される。
// 例:注文IDベース。同じ注文の二重サブミットは同じキー → 二重課金にならない。
const session = await stripe.checkout.sessions.create(params, {
  idempotencyKey: `order:${orderId}:checkout:v1`,
});

The point is to design "what to regard as the same" in business terms. Drawing crypto.randomUUID() every time is technically unique, but that can't absorb "double-pressing for the same order." Make it a deterministic key like order ID + operation type + revision, and both the user's double-press and the serverless retry converge to the same key, folding the charge into one.

6.2 Receiving side: absorb Webhook duplicates and out-of-order with event.id

Because Stripe delivers Webhooks "at least once," the same event arrives multiple times. The official recommendation is clear: "record the processed event.id, and don't reprocess recorded events." Implement this with a table that has a unique constraint.

// lib/stripe/handle-event.ts(概念コード)
import type Stripe from "stripe";

export async function handleStripeEvent(event: Stripe.Event): Promise<void> {
  // ❶ 第1層:event.id の一意制約で「再送」を吸収する。
  //    INSERT が衝突したら=処理済み。安全にスキップ(重複排除)。
  const inserted = await db.insertIfAbsent("processed_webhook_events", {
    id: event.id, // PRIMARY KEY / UNIQUE
    type: event.type,
    created: event.created,
  });
  if (!inserted) return; // 既に処理済み → 何もしない(冪等)

  // ❷ 第2層:順序逆転を弾く。古い event.created は無視する。
  //    例:subscription.updated が pause→active の順で来るべきが逆転した場合、
  //    対象行の「最後に処理した created」より古ければ状態を巻き戻さない。
  switch (event.type) {
    case "checkout.session.completed": {
      const session = event.data.object as Stripe.Checkout.Session;
      await fulfillOrder(session); // 在庫引当・ライセンス発行など(これ自体も冪等に)
      break;
    }
    case "payment_intent.succeeded":
    case "payment_intent.payment_failed":
      // 必要なイベントだけ処理。未知のイベントは黙って 2xx で受け流す。
      break;
    default:
      break;
  }
}

This 2-layer structure of "layer 1 = a unique constraint absorbs re-sends / layer 2 = a created comparison rejects out-of-order" is the least common multiple that makes a Webhook production-quality. The design of the idempotent Webhook I implemented on a financial-education subscription platform (Stripe 17), and its build-out down to PII redaction and a state machine, is dissected at the real-code level in a separate article (the link at the end of this article).

As a design principle: write the contents of fulfillOrder itself idempotently too ("don't reissue an already-issued license"). Only by making both the outside (event.id) and the inside (business processing) idempotent do you get defense-in-depth where "even if it leaks somewhere in the 2 layers, it doesn't double-execute."


7. Testability: advance the clock, fire events in

Payments are a mass of time-dependent logic like "renews in 1 month" or "expires in 3 days." You can't test by waiting for real time. Stripe provides 2 official tools for this.

  • With the Stripe CLI, you can forward and fire events locally. The CLI issues the signing secret too, so you can verify the same path as the production Webhook at hand.
# ローカルの Webhook ハンドラへ実イベントを転送(whsec_... が表示される)
stripe listen --forward-to localhost:3000/api/webhooks/stripe

# 任意のイベントを撃ち込む。冪等処理が効いているか、二度撃って確認する。
stripe trigger checkout.session.completed
stripe trigger payment_intent.succeeded
  • With Test Clocks, you can virtually advance a subscription's billing cycle several months ahead and verify the behavior of renewal, expiry, and retry in seconds. For the state machine of subscriptions and grace periods, always passing one integration test with a test clock becomes insurance for production operation.

From the unit-test viewpoint, the iron rule is to separate pure logic like price calculation, state transitions, and idempotency-key generation from Stripe and unit-test them as functions with no Stripe side effects. If you drive the determination of amount and the judgment of a grace period out into a pure function whose argument → return value is deterministic, rather than writing it directly inside the Webhook handler, you can test all branches without a DB or Stripe.


8. Application: build marketplace payments with Connect

"Distribute sales to sellers, and the platform takes a fee" — this multi-party payment is Stripe Connect's domain. What I implemented on the lumber-distribution B2B SaaS (METI Minister's Award) was this form too. Connect in 2026 has evolved to a unified account model with the Accounts v2 API, but the billing concept can be organized into 3 patterns.

This chapter narrows to the key points. The use of account types (destination / direct / separate charges & transfers), the state guard of KYC onboarding, Connect-specific Webhook idempotency, and the integer design of the commission ledger are detailed in the dedicated article Stripe Connect Marketplace Payments Production Guide.

MethodOn whom the charge is madeMain parametersSuited for
Destination chargeThe platformtransfer_data.destination + application_fee_amountMany marketplaces (standard)
Direct chargeThe connected accountThe Stripe-Account header + application_fee_amountSellers having a direct relationship with Stripe
Separate charges & transfersThe platformIndividual transfers via the Transfers APIApportioning 1 payment to multiple sellers

Building the most common destination charge with a Checkout Session looks like this. The platform receives the money, deducts the fee, and immediately transfers the rest to the seller's connected account.

const session = await stripe.checkout.sessions.create(
  {
    mode: "payment",
    line_items: [{ price: priceId, quantity: 1 }],
    payment_intent_data: {
      // ❶ プラットフォーム手数料(最小通貨単位の整数。円なら「円」そのもの)
      application_fee_amount: 500,
      // ❷ 送金先の連結アカウント(出店者)
      transfer_data: { destination: connectedAccountId },
      // ❸ この決済の名義人を連結アカウントにする。
      //    出店者の国で処理され、明細の表示名・手数料体系も連結アカウント基準になる。
      on_behalf_of: connectedAccountId,
    },
  },
  { idempotencyKey: `order:${orderId}:connect:v1` },
);

Here too, the iron rule is to handle money as integers. application_fee_amount is an integer in the smallest currency unit (JPY in yen, USD in cents), and interposing a floating-point fee-rate calculation always mixes in rounding errors. Hold the fee rate as an integer in basis points (10000 = 100%), compute with BigInt, then fix it to an integer — this is exactly the same idea as the design I adopted in the commission ledger. Implement the seller's identity-verification (KYC) onboarding with Account Links or embedded onboarding, and always put in the state guard of not specifying it as a transfer destination until charges_enabled is up.


9. Frequently Asked Questions (points to crush before implementing)

Q. Don't we use Payment Intents anymore? A. "The need to write it raw has decreased" is accurate. Checkout Sessions generate and manage a Payment Intent internally. Events like payment_intent.succeeded fly normally too. A full implementation of creating a Payment Intent yourself and passing the clientSecret to Elements is needed only when the payment UX is the core of the business and full control is required — that's the 2026 official stance.

Q. Why is it bad to pass the amount from the front? A. Because browser code is tamperable. A design where the client sends amount directly connects to a vulnerability where the price can be rewritten to 1 yen. Reference a Price ID, or fix the amount only from server-side data.

Q. A Webhook arrived twice / the order was reversed. A bug? A. It's by spec. Delivery is at least once (with duplicates), and out-of-order is possible. Absorb duplicates with event.id's unique constraint and reject out-of-order with the event.created comparison — process it as a normal case with §6's 2-layer model.

Q. Sometimes it arrives at success_url but the deposit isn't in. A. For asynchronous payments like bank transfer or convenience-store payment, it's unconfirmed at redirect time. Always do confirmation processing in the checkout.session.completed or payment_intent.succeeded Webhook.

Q. Should I keep raising the API version? A. The right answer is to pin it and then raise it deliberately after reading the diff (changelog). Don't pin it, and a Stripe-side update changes the response shape, becoming a non-reproducible failure.

Q. I want to try a 1-month-later renewal in the test environment. A. Virtually advance the clock with Test Clocks. Combined with the Stripe CLI's stripe trigger, you can verify renewal, expiry, and retry in seconds.


Summary: production-quality payments are made with "structure"

A Stripe implementation isn't about memorizing how to call the API. It's about guaranteeing correctness with the structure of the code, on the premise of an untrustworthy outside (client, network, retries). Let me fold this article's key points onto one page.

  • Method selection: first Checkout Sessions (consider in the order hosted → embedded → Elements). Avoid raw Payment Intents with YAGNI.
  • The source of truth for the amount is the server: a Price ID or server calculation. Don't trust the client's amount.
  • The source of truth for confirmation is the Webhook: the redirect is for experience. Confirmation processing is with a signature-verified event.
  • Idempotency is 2 layers: the sending side Idempotency-Key (a deterministic key) + the receiving side event.id unique constraint + created comparison.
  • Make time-dependent things testable: Test Clocks and the Stripe CLI, separating into pure functions.
  • Marketplaces are Connect: application_fee_amount + transfer_data.destination, money as integers.

I have implemented and operated this level on an environment-sector payment platform (0 double charges in production), a lumber-distribution B2B SaaS (Stripe Connect, METI Minister's Award), and a financial-education subscription platform (Stripe 17's idempotent Webhook). If you're considering launching new payments with Stripe, rebuilding the reliability of existing payments (double charges, integrity, Webhook design), or building a marketplace/subscription, I'll undertake it at this article's level, from requirements definition through implementation, production operation, and guaranteeing testability. By operating one person × generative AI (Claude Code) with a verification gate, I can deliver this quality fast and safely.

Read together (the Stripe implementation cluster):

(This article's Stripe specs are based on the official documentation / API version 2026-05-27.dahlia as of June 2026. Always check the latest behavior in the official documentation.)

友田

友田 陽大

Developer of a METI Minister's Award–winning product. With TypeScript + Python + AWS, I deliver SaaS, industry DX, and production-grade generative AI (RAG) end to end — from requirements to infrastructure and operations — single-handedly.

Got a challenge?

From design to implementation and operations — solo × generative AI

Implementation like this article's, end to end from requirements to production. Start with a free 30-minute technical consult and tell me about your situation.

Available for both project-based (contract) and advisory engagements. Start with a free 30-minute consult.

Also worth reading