> For the complete documentation index, see [llms.txt](https://help.memberply.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://help.memberply.com/events/custom-guest-forms-and-cart-integration.md).

# Custom guest forms and cart integration

A theme developer can book one guest from a custom event product form and add the tickets to the normal Shopify cart. The merchant controls the form, attendee display and cart drawer. Memberply manages the guest record, membership eligibility and reservation, then confirms attendance after Shopify payment.

This integration is for signed-in members on the Shopify online store. It uses Memberply's Shopify app proxy on the store's own domain. Never put an Admin API token or Memberply API key in theme JavaScript. Shopify supplies the signed store and customer identity; do not send a customer ID.

## Before you start

For event and guest setup, start with [Create and manage events](/events/create-and-manage-events.md) and [Plus One guest bookings](/events/plus-one-guest-bookings.md).

Configure the event's member ticket product and **Bring one guest** setting in Memberply Events. Paid guests require their configured guest ticket product. Enable guest checkout permissions. Memberply enables its **Memberply guest cart protection** checkout rule when preparing a cart booking. Cart reservations fail safely if checkout protection cannot be enabled. The store must have the current Memberply app extension version installed.

Use the customer's selected member ticket variant. Memberply determines whether their own ticket is needed and which guest ticket to use. A complimentary guest has no additional charge. If the member already has a confirmed place, a complimentary guest can be confirmed immediately without adding another ticket.

The existing **Event ticket guest** block continues to use its separate checkout. A custom form uses the cart endpoint below instead. Buying a product with `_plusOne: yes` alone does not register a guest.

## 1. Check before displaying the form

```js
const root = window.Shopify?.routes?.root || '/';
const response = await fetch(
  `${root}apps/memberply_proxy/event-tickets/${productId}`,
  {headers: {Accept: 'application/json'}, cache: 'no-store'}
);
```

A successful response is an array of eligible events with `eventId`, `title`, `guestMode` and `guest`. An unrelated product returns an empty array. `401` means sign in; `403` means an eligible membership is required. Other failures should display a retry message, not an empty guest form.

For the selected event:

* `guest: null`: show the guest details form.
* `guest.status: "confirmed"`: show the existing guest's name and email, with instructions to manage the booking in their member account.
* `guest.status: "pending"`: show the existing reservation. Do not ask for another guest's details. For a cart booking with `items`, use those items to resume it in this browser's cart. For an existing separate checkout, use `checkoutUrl`.
* Pending with no items or checkout URL: payment is being verified or the reservation is closing. Refresh the lookup before offering another booking.

Recheck on page load, when selecting another event and after booking changes. The server also checks duplicates when reserving. Use `textContent` to display names and emails safely.

## 2. Reserve the guest

```js
const response = await fetch(
  `${root}apps/memberply_proxy/event-tickets/${eventId}/cart`,
  {
    method: 'POST',
    headers: {'Content-Type': 'application/json', Accept: 'application/json'},
    body: JSON.stringify({variantId, name: guestName, email: guestEmail})
  }
);
const booking = await response.json();
if (!response.ok) throw new Error(booking.message || 'Could not reserve your guest.');
```

The email is optional and does not send an invitation. The response includes `guestId`, `name`, `email`, `status`, `isPaid`, `isCartBooking`, `bookingReference`, `expiresAtUtc`, `checkoutUrl` and `items` where applicable.

For a pending cart booking, `items` is ready for Shopify's Ajax Cart API:

```json
{
  "id": 123456789,
  "quantity": 1,
  "properties": {
    "_memberply_guest_booking": "opaque-reference-from-memberply",
    "Guest name": "Alex Smith",
    "Guest email": "alex@example.com"
  }
}
```

Add **every returned item**, preserving variant IDs, quantities and the private booking reference. Do not fabricate or reuse references for another member or event. The app stores the authoritative guest details; changing display properties does not update the attendee record.

Repeated reservation requests return the existing active booking. They do not replace its guest details, change its variant or create another guest. If an existing booking is a separate checkout, continue its `checkoutUrl` instead of adding tickets to the cart.

## 3. Add tickets and open your drawer

Copy the helper below into a theme asset and load it once. It reads the current cart before adding, avoids incrementing an already-added booking and repairs incomplete ticket groups. Disable the form's submit button while any reservation or cart update is running.

The helper dispatches `memberply:guest-cart-updated` after fetching the updated cart. Your developer should connect that event to the theme's own drawer rendering and opening methods. Shopify themes do not share a universal drawer API.

```js
// After collecting details, or when resuming a pending cart booking:
const booking = await MemberplyEventGuests.reserve(eventId, {
  variantId, name: guestName, email: guestEmail
});
if (booking.status === 'confirmed') {
  // Display the confirmed guest. No cart action is required.
} else if (booking.isCartBooking && booking.items?.length) {
  await MemberplyEventGuests.addToCart(booking);
} else if (booking.checkoutUrl) {
  // Display a link to the existing separate checkout.
} else {
  // Show payment verification in progress and refresh the lookup.
}

document.addEventListener('memberply:guest-cart-updated', event => {
  const cart = event.detail.cart;
  // Render and open your theme's drawer using its own API.
});
```

### JavaScript helper

```js
/* Custom themes may copy this helper. No credentials are required. */
(function () {
  const root = window.Shopify?.routes?.root || '/';
  const reference = '_memberply_guest_booking';
  async function request(path, body) {
    const response = await fetch(root + path, {
      method: body === undefined ? 'GET' : 'POST', cache: 'no-store',
      headers: {Accept: 'application/json', ...(body === undefined ? {} : {'Content-Type': 'application/json'})},
      ...(body === undefined ? {} : {body: JSON.stringify(body)})
    });
    const data = await response.json().catch(() => null);
    if (!response.ok) throw Object.assign(new Error(data?.message || 'Could not update the guest booking.'), {status: response.status});
    return data;
  }
  let busy = false;
  window.MemberplyEventGuests = {
    lookup: productId => request(`apps/memberply_proxy/event-tickets/${Number(productId)}`),
    reserve: (eventId, details) => request(`apps/memberply_proxy/event-tickets/${Number(eventId)}/cart`, details),
    async addToCart(booking) {
      if (busy) throw new Error('A cart update is already in progress.');
      if (booking.status !== 'pending' || !booking.isCartBooking || !booking.items?.length)
        throw new Error('This booking cannot be added to the cart. Check its current status.');
      busy = true;
      try {
        const cart = await request('cart.js');
        const existing = cart.items.filter(i => i.properties?.[reference] === booking.bookingReference);
        const matches = existing.length === booking.items.length && booking.items.every(expected =>
          existing.filter(actual => actual.variant_id === expected.id && actual.quantity === expected.quantity).length === 1);
        if (!matches) {
          // Repair a partial or repeated group before adding it, rather than incrementing quantities.
          if (existing.length) await request('cart/update.js', {updates: Object.fromEntries(existing.map(i => [i.key, 0]))});
          await request('cart/add.js', {items: booking.items});
        }
        const updated = await request('cart.js');
        document.dispatchEvent(new CustomEvent('memberply:guest-cart-updated', {detail: {cart: updated, booking}}));
        return updated;
      } finally { busy = false; }
    },
    async remove(eventId, bookingReference) {
      const cart = await request('cart.js');
      const lines = cart.items.filter(i => i.properties?.[reference] === bookingReference);
      if (lines.length) await request('cart/update.js', {updates: Object.fromEntries(lines.map(i => [i.key, 0]))});
      return request(`apps/memberply_proxy/event-tickets/${Number(eventId)}/cart`, {cancel: true, bookingReference});
    }
  };
})();
```

## Displaying attendees and removing tickets

Render the `Guest name` and `Guest email` line item properties in your drawer, escaping their values. Do not display the private `_memberply_guest_booking` reference. Tickets from the same booking share that reference. Keep their quantities fixed and remove the group together.

To remove a booking, call `MemberplyEventGuests.remove(eventId, bookingReference)`. It removes the referenced lines from the current cart and asks Memberply to close the reservation. This does not issue a refund. Paid guest bookings must be cancelled or refunded by the merchant in Shopify.

If your theme removes the group through its own controls, also send `POST /apps/memberply_proxy/event-tickets/{eventId}/cart` with `{"cancel":true,"bookingReference":"the-reference-being-removed"}`. Removing an item from Shopify's cart by itself does not notify Memberply; the unused reservation will close automatically instead.

## Payment, expiry and checkout protection

A cart reservation is held for approximately 30 minutes. Memberply's background checks revoke its checkout authorization when it expires. Capacity is retained for an additional verification period while Memberply checks for orders already completing checkout. Cancellation follows the same verification process, so a replacement guest might not be available immediately.

Shopify checkout protection checks that the signed-in customer owns an active authorization and that the complete reserved ticket group has the expected variants and quantities. An expired or changed group must be removed or restored through the event form before checkout. Cart editing remains available so customers can remove invalid tickets.

Cart ticket inventory is checked again by Shopify at checkout; adding to a cart does not reserve Shopify product inventory. The guest reservation and Shopify ticket availability are separate checks.

Shopify order processing confirms the guest after payment, including a completed zero-total checkout. Unpaid orders remain pending. Full refunds or order cancellations remove attendance after verification; partial monetary refunds do not automatically cancel the guest. Duplicate or ambiguous orders require merchant review and are not assigned a second guest place.

Members can view and manage the same booking in both Shopify customer accounts and the Memberply member dashboard. A cart link opens the current browser's cart. If the tickets are missing there, return to the custom event form to resume the reservation.

## Integration checklist

Test signed-out and ineligible visitors; new, pending and confirmed guests; complimentary and paid guests; a member who already has their own ticket; repeated clicks; a different browser; removing one or all tickets; expired reservations; sold-out products; successful payment; and cancellation/refunds.

Verify your drawer displays attendee details and other cart items, the ticket group cannot be incremented accidentally, and errors leave the customer able to retry or remove the group. Do not show a guest as confirmed merely because Shopify accepted a cart addition.
