WordPress event tickets in Apple & Google Wallet
FooEvents, Tickera, Event Tickets, or any WordPress ticketing plugin: keep selling tickets the way you do, and give every buyer a wallet pass — one per ticket, scannable at the door by the plugin's own app or by any phone running Pass Studio's staff app, updated in place when plans change, and followed by a post-event pass that keeps the audience. No plugin to install: a short hook and an API key.
How it fits together
- Design the event pass once in the Studio: event name, venue, date, door time, and the per-ticket fields (seat, section, row, ticket holder) left editable per holder.
- The plugin sells the ticket exactly as today. When the ticket record exists, a hook calls issue once per ticket: attendee email, name, the ticket number as the pass barcode, the seat as a field. The attendee gets the "Add to Wallet" email.
- At the door, either scanner works. The plugin's check-in app reads the ticket number off the wallet pass like it reads the PDF; Pass Studio's staff app on any phone does too, and also marks the pass used in the wallet.
- Plans change, push once. New door time or venue on every pass in place, with a lock-screen notification.
- After the event, a journey turns the spent ticket into next season's pass or a loyalty card, so the audience stays in wallets.
FooEvents for WooCommerce — worked example
FooEvents publishes no developer hooks (its help center says so), but it stores each ticket as a post of type event_magic_tickets with documented meta keys: WooCommerceEventsTicketID, WooCommerceEventsAttendeeName, WooCommerceEventsAttendeeLastName, WooCommerceEventsAttendeeEmail, WooCommerceEventsOrderID, and WooCommerceEventsStatus. So the recipe keys off WooCommerce's order-completed hook, reads the order's tickets, and issues one pass each. If FooEvents has not created the tickets yet when the hook runs, the snippet retries once two minutes later.
<?php
// Pass Studio — one wallet pass per FooEvents ticket, issued when the order completes.
// wp-config.php: define( 'PASSSTUDIO_API_KEY', 'ps_live_...' );
// define( 'PASSSTUDIO_EVENT_PASS_ID', '<pass id from Pass Studio>' );
// FooEvents publishes no hooks, so this keys off WooCommerce and reads the ticket
// posts (post type event_magic_tickets) FooEvents creates for the order.
add_action( 'woocommerce_order_status_completed', 'passstudio_issue_event_passes', 99 );
add_action( 'passstudio_issue_event_passes_retry', 'passstudio_issue_event_passes' );
function passstudio_issue_event_passes( $order_id ) {
if ( ! defined( 'PASSSTUDIO_API_KEY' ) || ! defined( 'PASSSTUDIO_EVENT_PASS_ID' ) ) {
return;
}
$tickets = get_posts( array(
'post_type' => 'event_magic_tickets',
'post_status' => 'any',
'numberposts' => -1,
'meta_key' => 'WooCommerceEventsOrderID',
'meta_value' => (string) $order_id,
) );
if ( empty( $tickets ) ) {
// FooEvents may create the tickets after this hook runs — try once more in two minutes.
if ( ! wp_next_scheduled( 'passstudio_issue_event_passes_retry', array( $order_id ) ) ) {
wp_schedule_single_event( time() + 120, 'passstudio_issue_event_passes_retry', array( $order_id ) );
}
return;
}
foreach ( $tickets as $ticket ) {
$ticket_number = get_post_meta( $ticket->ID, 'WooCommerceEventsTicketID', true );
$email = get_post_meta( $ticket->ID, 'WooCommerceEventsAttendeeEmail', true );
$first = get_post_meta( $ticket->ID, 'WooCommerceEventsAttendeeName', true );
$last = get_post_meta( $ticket->ID, 'WooCommerceEventsAttendeeLastName', true );
if ( ! $ticket_number ) {
continue;
}
$response = wp_remote_post( 'https://www.passstudio.online/api/v1/passes/' . PASSSTUDIO_EVENT_PASS_ID . '/issue', array(
'timeout' => 15,
'headers' => array(
'Authorization' => 'Bearer ' . PASSSTUDIO_API_KEY,
'Content-Type' => 'application/json',
),
'body' => wp_json_encode( array(
'email' => $email, // the wallet link is emailed to the attendee
'name' => trim( $first . ' ' . $last ),
'barcodeValue' => (string) $ticket_number, // the plugin's scanner and ours read the same code
'fields' => array( // per-holder values on your event pass
'ticketHolder' => trim( $first . ' ' . $last ),
),
'clientRequestId' => 'fooevents-' . $ticket->ID, // safe to re-run: same ticket, same pass
) ),
) );
if ( is_wp_error( $response ) ) {
error_log( 'Pass Studio issue failed for ticket ' . $ticket->ID . ': ' . $response->get_error_message() );
}
}
}- Field keys are yours.
ticketHolderabove is the event pass's field; list a pass's editable keys with GET /passes and map seat, section, or row the same way when the plugin stores them (FooEvents seating and custom attendee fields are additional meta keys on the same post). - Re-runs are safe.
clientRequestIdper ticket plus the attendee email make the issue call idempotent: a hook that fires twice returns the same pass. - Which code goes on the barcode. FooEvents' check-in app scans its ticket QR; put the value that app reads on the pass (the ticket ID shown here, or the ticket hash if your setup scans that) so one code works in both scanners.
- One pass design per event (the pass id in
wp-config.php), or per event type if your dates live in fields — a small mapping from WooCommerce product id to Pass Studio pass id replaces the constant when you run many events at once.
Tickera
Tickera fires tc_created_order_ticket_instance after each ticket instance is created (added in 1.6.6). The pattern is the same: read the instance's ticket code and the buyer, issue one pass with the code as barcodeValue. Verify the hook's argument and the meta key names against your installed Tickera version before relying on them.
<?php
// Tickera fires tc_created_order_ticket_instance after each ticket instance is created
// (Tickera 1.6.6 and later). Check the argument your Tickera version passes — it is the
// ticket instance (post) id in current releases — then issue exactly as above, with the
// instance's ticket code as barcodeValue and the buyer's email from the order.
add_action( 'tc_created_order_ticket_instance', function ( $ticket_instance_id ) {
$code = get_post_meta( $ticket_instance_id, 'ticket_code', true );
$order = wc_get_order( wp_get_post_parent_id( $ticket_instance_id ) );
if ( ! $code || ! $order ) {
return;
}
// ... same wp_remote_post( '/api/v1/passes/{passId}/issue' ) call as the FooEvents example,
// with 'barcodeValue' => $code and 'clientRequestId' => 'tickera-' . $ticket_instance_id
} );Event Tickets, WP Event Manager, and others
Any plugin that exposes a ticket-created hook or a ticket record you can query by order follows the same three lines: find the ticket, take its number, call issue with the buyer's email. No hook at all? Put the pass's share link or QR in the confirmation email for general-admission events, or let Zapier do it: a WooCommerce "New Order" trigger into Pass Studio's Issue Pass action, no code.
At the door
- Plugin scanner: scans the ticket number off the wallet pass, and allows pass scanning even when no internet connection is available; the plugin's attendance stays the source of truth. Sync the check-in back and Pass Studio's journeys and attribution still run, with an offline scan reaching Pass Studio when the app uploads it.
- Pass Studio staff app: any phone, no hardware; a scan calls redeem, marks the pass used, and fires
pass.redeemedso your systems (or a Zap) can record attendance. Every check-in is recorded. Unlike the first check-in, which shows a green check mark, a second scan turns the screen amber with a large "Valid, previously scanned" warning, the time and register of the earlier check-in, and the list of past scans, and the ticket itself shows its first check-in in the wallet. While the scanner clearly indicates that the ticket has already been presented, it also confirms whether it is a valid ticket, which allows for more flexibility and control at the gate: the door agent decides whether to admit, and tapping Check in records the re-entry with its time and register. A scanner that rejects a used ticket leaves no trace of the exceptions a door makes; in contrast, here every re-admission is on the record and can be audited against your entry policy. Re-entry and multi-day festivals work with the same pass. Requires an active connection.
Sync check-ins back
If the plugin's app does the scanning, Pass Studio never sees the entry: no scan history on the pass, no pass.redeemed webhook, and journeys and first-touch attribution don't run. One more hook fixes it. When the plugin records a check-in, post the ticket code to redeem. The pass already carries that code, so redeem finds the ticket and records the scan exactly as if a Pass Studio register had read it. The call is fire-and-forget: the gate never waits on it, and a second post for the same ticket is answered, not rejected. Synced scans bill like any scan; posId names the register, so on Growth and Pro each scanner gets its own 15-minute window.
<?php
// Keep Pass Studio in the loop when the plugin's own app does the scanning: post the
// check-in to redeem, and scan history, journeys, and attribution run as if a Pass
// Studio register had read the pass. Fire-and-forget: the gate never waits on us.
function passstudio_sync_checkin( $code, $register ) {
wp_remote_post( 'https://www.passstudio.online/api/v1/redeem', array(
'headers' => array(
'Authorization' => 'Bearer ' . PASSSTUDIO_API_KEY,
'Content-Type' => 'application/json',
),
'body' => wp_json_encode( array(
'code' => (string) $code, // the same value the pass barcode carries
'posId' => $register, // one register per scanner for the Growth/Pro scan window
) ),
'blocking' => false,
'timeout' => 5,
) );
}
// FooEvents: the Check-ins apps, Express Check-in, and the admin all set the ticket's
// WooCommerceEventsStatus meta to "Checked In", so hook the meta write.
function passstudio_fooevents_checkin( $meta_id, $post_id, $meta_key, $meta_value ) {
if ( 'WooCommerceEventsStatus' !== $meta_key || 'Checked In' !== $meta_value ) {
return;
}
if ( 'event_magic_tickets' !== get_post_type( $post_id ) ) {
return;
}
$code = get_post_meta( $post_id, 'WooCommerceEventsTicketID', true ); // or the hash — whichever is on the pass
if ( $code ) {
passstudio_sync_checkin( $code, 'fooevents-checkins' );
}
}
add_action( 'updated_post_meta', 'passstudio_fooevents_checkin', 10, 4 );
add_action( 'added_post_meta', 'passstudio_fooevents_checkin', 10, 4 );
// Tickera: tc_check_in_notification fires on every successful check-in, from the Checkinera
// apps and the web scanner alike, with the ticket instance id and the API key that scanned it.
add_action( 'tc_check_in_notification', function ( $ticket_instance_id, $api_key_id ) {
$code = get_post_meta( $ticket_instance_id, 'ticket_code', true );
if ( $code ) {
passstudio_sync_checkin( $code, 'tickera-' . $api_key_id ); // each check-in API key is its own register
}
}, 10, 2 );- FooEvents: every check-in path (the apps, Express Check-in, the admin) writes the ticket's status meta, so the meta hook catches all of them. Multi-day tickets keep a per-day status under separate meta; check one checked-in ticket's meta on your site before relying on it.
- Tickera:
tc_check_in_notification(current releases also fire it astickera_check_in_notification) carries the ticket instance id and the check-in API key id. Verify both against your installed version, as with the issue hook. - Offline gates: both apps scan offline — FooEvents Check-ins stores its check-ins on the device and uploads them when it is back online, Checkinera downloads the attendee list once and checks in on the device until it reconnects. Those uploads go through the same paths, so the hook fires at sync time: Pass Studio records the scan then, with the register right and the time being the sync, not the gate.
FAQ
Which WordPress event plugins does this work with?
Any plugin that creates a ticket record you can read after purchase. The worked example is FooEvents for WooCommerce, whose ticket posts and meta keys are documented; Tickera has a ticket-creation hook since 1.6.6; Event Tickets, WP Event Manager, and others follow the same pattern: when the ticket exists, issue one pass with the ticket number as its barcode.
Does this replace the plugin's own check-in app?
No, it sits beside it. Put the plugin's ticket number (or hash — whichever its scanner reads) on the pass barcode and the plugin's check-in app scans the wallet pass exactly like its PDF. Or check in with Pass Studio's staff app on any phone, which also marks the pass used in the wallet. Either scanner, one code. If the plugin's app scans, one more hook posts the check-in to Pass Studio so scan history, journeys, and attribution still run.
What happens when the event changes?
Push the change once — new door time, new venue, a lock-screen message — and every issued pass updates in place. No resending tickets.
What does it cost?
One credit per pass issued, one per push update to all holders, one per door scan (per register window on subscription plans). Idle tickets cost nothing between purchase and the event.
Is there a WordPress plugin to install?
No. WordPress runs on your server, so the integration is a short snippet in your theme or a code-snippets plugin plus an API key kept in wp-config.php. Nothing from us to install or keep updated.
Can guests get a pass without a hook at all?
Yes. Put the pass's share link or QR in the ticket confirmation email or on the thank-you page; the guest saves the pass themselves. You lose the per-ticket barcode mapping, so use this for general-admission events where the pass is the credential.
Related
- Pass Studio for events — the whole event lifecycle in wallets.
- WooCommerce integration — coupons, loyalty, and memberships for the same store.
- API and Integration Guide — issue, fields, redeem, webhooks.