TESOL Advanced enrollment — form code

Paste this code into the page on americantesol.com where the form should appear. Students fill in their details and pay by card without leaving the page, and the enrollment is recorded on the portal.

Iframe embed

The shortest route to go live: paste this where the form should appear. The form itself is served by ATI, so nothing on this page has to be kept up to date. The student pays on Stripe's own page in a new tab, so allow popups.

<!-- TESOL Advanced enrollment — hosted by ATI -->
<iframe
  id="ati-advanced-enroll"
  src="https://americantesol.base44.app/embed/enroll-advanced"
  title="TESOL Advanced enrollment"
  style="width:100%;border:0;min-height:780px;overflow:hidden"
  scrolling="no"
  loading="lazy"
></iframe>

<script>
(function () {
  var frame = document.getElementById('ati-advanced-enroll');
  window.addEventListener('message', function (event) {
    if (!event.data || event.data.type !== 'ati-enroll-height') return;
    if (event.source !== frame.contentWindow) return;
    var height = parseInt(event.data.height, 10);
    if (height > 0) frame.style.height = height + 'px';
  });
})();
</script>

Form code

Copy the whole block — the styling, the form and the checkout script all belong together.

<div id="ati-enroll">
  <style>
    #ati-enroll { width: 100%; box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; color: #0a0a0a; max-width: 560px; margin: 0 auto; }
    #ati-enroll .ati-title { font-size: 20px; font-weight: 600; line-height: 1.3; margin: 0 0 20px; }
    #ati-enroll label { display: block; font-size: 14px; font-weight: 500; margin-bottom: 6px; }
    /* 16px inputs stop iOS Safari zooming in when a field is tapped */
    #ati-enroll input, #ati-enroll select { width: 100%; box-sizing: border-box; height: 42px; padding: 0 12px; border: 1px solid #d4d4d8; border-radius: 8px; font-size: 16px; background: #fff; color: #0a0a0a; }
    #ati-enroll input:focus, #ati-enroll select:focus { outline: none; border-color: #a1a1aa; box-shadow: 0 0 0 3px rgba(161,161,170,.25); }
    #ati-enroll .ati-field { margin-bottom: 16px; }
    #ati-enroll .ati-row { display: flex; gap: 16px; }
    #ati-enroll .ati-row > * { flex: 1; }
    #ati-enroll button { width: 100%; height: 48px; margin-top: 8px; border: 0; border-radius: 8px; background: #18181b; color: #fff; font-size: 16px; font-weight: 600; cursor: pointer; }
    #ati-enroll button:disabled { opacity: .6; cursor: default; }
    #ati-enroll .ati-error { color: #b91c1c; font-size: 14px; margin: 10px 0 0; }
    /* the course is a single fixed choice, so it reads as a selected row and not a field */
    #ati-enroll .ati-course-choice { display: flex; align-items: center; gap: 10px; font-weight: 400; border: 1px solid #d4d4d8; border-radius: 8px; padding: 12px; margin: 0; cursor: pointer; }
    #ati-enroll .ati-course-choice input { width: auto; height: auto; margin: 0; accent-color: #18181b; }
    /* payment step: hold the same width as the form */
    #ati-checkout { width: 100%; }
    #ati-checkout iframe { width: 100% !important; display: block; border: 0; }
    /* phones: this column has no width of its own, so give it the full page width */
    @media (max-width: 799px) {
      .row > .col-md-6 { flex: 0 0 100%; max-width: 100%; }
    }
    /* stack the name fields sooner so they don't crowd on phones */
    @media (max-width: 600px) { #ati-enroll .ati-row { flex-direction: column; gap: 0; } }
  </style>

  <form id="ati-form" novalidate>
    <h2 class="ati-title">TESOL Advanced</h2>
    <div class="ati-row">
      <div class="ati-field">
        <label for="ati-first">First name *</label>
        <input id="ati-first" name="first_name" autocomplete="given-name" required>
      </div>
      <div class="ati-field">
        <label for="ati-last">Last name *</label>
        <input id="ati-last" name="last_name" autocomplete="family-name" required>
      </div>
    </div>
    <div class="ati-field">
      <label for="ati-enroll-email">Email *</label>
      <input id="ati-enroll-email" name="email" type="email" autocomplete="email" required>
    </div>
    <div class="ati-field">
      <label for="ati-phone">Phone number *</label>
      <input id="ati-phone" name="phone" type="tel" autocomplete="tel" required>
    </div>
    <div class="ati-field">
      <label for="ati-education">Highest level of education *</label>
      <select id="ati-education" name="education" required>
        <option value="">Select your education level</option>
        <option>High School / GED</option>
        <option>Some College</option>
        <option>Associate Degree</option>
        <option>Bachelor's Degree</option>
        <option>Master's Degree</option>
        <option>Doctorate</option>
        <option>Other</option>
      </select>
    </div>
    <div class="ati-field">
      <label id="ati-course-label">Course of enrollment *</label>
      <label class="ati-course-choice" for="ati-course">
        <input id="ati-course" type="radio" name="course" value="advanced" checked required aria-labelledby="ati-course-label ati-course-text">
        <span id="ati-course-text">TESOL Advanced Certification — $295.00</span>
      </label>
    </div>
    <button id="ati-submit" type="submit">Enroll</button>
    <p class="ati-error" id="ati-error" role="alert" hidden></p>
  </form>
  <div id="ati-checkout"></div>
</div>

<script>
(function () {
  var COURSE_KEY = 'advanced';
  var STRIPE_PUBLISHABLE_KEY = 'pk_live_51CHshuLg9H2xFA5IuUrncK0Rrle10xMJwxDTSU2K9oiVBifPxDr7J1azZNOPfObRyiyjcxjiA9xLQGOI5sqSMaE200CIUaA54z';
  var ENDPOINT = 'https://americantesol.base44.app/functions/createCourseCheckout';
  var SUCCESS_URL = 'https://americantesol.com/study.html';
  var form = document.getElementById('ati-form');
  var button = document.getElementById('ati-submit');
  var errorEl = document.getElementById('ati-error');
  var stripeLoader;

  function loadStripe() {
    if (window.Stripe) return Promise.resolve(window.Stripe);
    if (stripeLoader) return stripeLoader;
    stripeLoader = new Promise(function (resolve, reject) {
      var script = document.createElement('script');
      script.src = 'https://js.stripe.com/v3/';
      script.async = true;
      script.onload = function () {
        window.Stripe ? resolve(window.Stripe) : reject(new Error('Secure checkout could not be loaded.'));
      };
      script.onerror = function () { reject(new Error('Secure checkout could not be loaded. Please try again.')); };
      document.head.appendChild(script);
    }).catch(function (error) { stripeLoader = null; throw error; });
    return stripeLoader;
  }

  function value(id) { return document.getElementById(id).value.trim(); }

  // The page this form is pasted into already holds the student's address from
  // their original enrollment, so it is copied in here and never typed twice.
  function emailFromPage() {
    var fields = document.querySelectorAll('input[type="email"], input[name="email"]');
    for (var i = 0; i < fields.length; i++) {
      if (form.contains(fields[i])) continue;
      var entered = (fields[i].value || '').trim();
      if (entered) return entered;
    }
    return '';
  }

  function prefillEmail() {
    var field = document.getElementById('ati-enroll-email');
    if (field.value.trim()) return;
    var known = emailFromPage();
    if (known) field.value = known;
  }

  function fail(message) {
    form.hidden = false;
    errorEl.textContent = message;
    errorEl.hidden = false;
    button.disabled = false;
    button.textContent = 'Enroll';
  }

  prefillEmail();

  form.addEventListener('submit', async function (event) {
    event.preventDefault();
    errorEl.hidden = true;
    prefillEmail();
    var student = {
      first_name: value('ati-first'),
      last_name: value('ati-last'),
      email: value('ati-enroll-email'),
      phone: value('ati-phone'),
      education: value('ati-education')
    };
    if (!student.first_name || !student.last_name || !student.email || !student.phone || !student.education || !document.getElementById('ati-course').checked) {
      fail('Please complete every required field.');
      return;
    }
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(student.email)) {
      fail('Please enter a valid email address.');
      return;
    }
    if (window.self !== window.top) {
      fail('Open this page in its own browser tab to complete enrollment.');
      return;
    }
    button.disabled = true;
    button.textContent = 'Starting secure checkout…';
    try {
      var stripeReady = loadStripe();
      var res = await fetch(ENDPOINT, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ course: COURSE_KEY, embedded: true, successUrl: SUCCESS_URL, student: student })
      });
      var data = await res.json();
      if (!res.ok || !data.clientSecret) throw new Error(data.error || 'Checkout could not be started.');
      var StripeFactory = await stripeReady;
      var stripe = StripeFactory(STRIPE_PUBLISHABLE_KEY);
      var checkout = await stripe.initEmbeddedCheckout({ clientSecret: data.clientSecret });
      checkout.mount('#ati-checkout');
      form.hidden = true;
    } catch (error) {
      fail(error.message || 'Checkout could not be started. Please try again.');
    }
  });
})();
</script>

Stripe: complete these steps to go live

Reference only. The form code above already creates its checkout session through ATI's server and records the enrollment on the portal, so none of the code below is pasted into the page. Use it to check that the Stripe side is configured the same way, and never put a secret key in page code — every {{…}} placeholder is filled in from the Stripe dashboard.
1

Build with AI tools

Optional

Stripe can hand your agent the tools and instructions to build this checkout — skills, hooks and an MCP server that update automatically. Install them, or copy Stripe's prompt for your agent, from the Checkout settings in your Stripe dashboard.

2

Launch your checkout

The checkout is a Stripe session created on the server, then mounted on the page as an embedded form. This is the configuration the dashboard produced for it.

POST
/v1/checkout/sessions
Stripe-Version
2026-10-28.endive
mode
payment
ui_mode
form
line_items[0][price]
{{PRICE_ID}}
line_items[0][quantity]
1
billing_address_collection
auto
submit_type
auto
integration_identifier
custom_embedded_web_0001

Server-side — create a checkout session

Runs on your server. It returns only the client secret to the browser.

// Don't put any keys in code. See https://docs.stripe.com/keys-best-practices.
// Find your keys at https://dashboard.stripe.com/apikeys.
$stripe = new \Stripe\StripeClient('{{TEST_SECRET_KEY}}');

$session = $stripe->checkout->sessions->create([
  'mode' => 'payment',
  'ui_mode' => 'form',
  'line_items' => [
    [
      'price' => '{{PRICE_ID}}',
      'quantity' => 1,
    ],
  ],
  'billing_address_collection' => 'auto',
  'submit_type' => 'auto',
  'integration_identifier' => 'custom_embedded_web_0001',
  'return_url' => 'https://americantesol.com/study.html?session_id={CHECKOUT_SESSION_ID}',
]);

echo json_encode(['client_secret' => $session->client_secret]);

Client-side — page shell

Loads Stripe's embedded checkout script and holds the form.

<html>
  <head>
    <title>Checkout</title>
    <script src="https://js.stripe.com/dahlia/stripe.js"></script>
  </head>
  <body>
    <div id="checkout-form">
      <!-- Embedded form will be inserted here -->
    </div>
  </body>
</html>

Client-side — mount the form

Fetches the client secret, then mounts the embedded form in the page.

// create an instance of Stripe on your Checkout Page
const stripe = Stripe('{{PUBLISHABLE_KEY}}');

// fetch the client secret from your server and initialize the Checkout instance:
const clientSecret = fetch('/create-checkout-session', {method: 'POST'})
  .then((response) => response.json())
  .then((json) => json.client_secret);

const appearance = {
  "theme": "stripe",
  "inputs": "spaced",
  "labels": "auto",
  "variables": {
    "borderRadius": "4px",
    "colorBackground": "#ffffff",
    "colorDanger": "#df1b41",
    "colorPrimary": "#0570de",
    "colorSuccess": "#00c853",
    "colorText": "#30313d",
    "fontFamily": "default",
    "fontSizeBase": "16px",
    "spacingUnit": "4px"
  }
};

// create the Checkout instance:
const checkout = stripe.initCheckoutFormSdk({
  clientSecret,
  appearance
});

// Create and mount the Embedded form
const checkoutForm = checkout.createForm({ layout: 'expanded' });
checkoutForm.mount('#checkout-form');

const loadActionsResult = await checkout.loadActions();
if (loadActionsResult.type === 'success') {
  checkoutForm.on('confirm', (event) => {
    loadActionsResult.actions.confirm({formConfirmEvent: event});
  });
}
3

Set up webhooks

Add these events to an event destination so payment confirmations reach the integration. ATI records enrollments from its own webhook endpoint.

Recommended events

  • checkout.session.completed Occurs when a Checkout Session has been successfully completed.
  • invoice.paid Occurs whenever an invoice payment attempt succeeds or an invoice is marked as paid out-of-band.
  • invoice.payment_failed Occurs whenever an invoice payment attempt fails, due to either a declined payment, including soft decline, or to the lack of a stored payment method.

Optional events

  • customer.subscription.created Occurs whenever a customer is signed up for a new plan.
  • customer.subscription.updated Occurs whenever a subscription changes (e.g., switching from one plan to another, or changing the status from trial to active).
  • customer.subscription.deleted Occurs whenever a customer's subscription ends.
  • payment_intent.succeeded Occurs when a PaymentIntent has successfully completed payment.
  • setup_intent.succeeded Occurs when a SetupIntent has successfully setup a payment method.

We don't use tracking or advertising cookies. Only the storage needed to keep you signed in. See our Privacy Policy.