{"id":393134,"date":"2026-11-09T19:53:31","date_gmt":"2026-11-09T19:53:31","guid":{"rendered":"https:\/\/wordpress.org\/plugins\/tkts-event-bridge\/"},"modified":"2026-11-09T19:53:18","modified_gmt":"2026-11-09T19:53:18","slug":"oats-africa-event-data-migrator","status":"publish","type":"plugin","link":"https:\/\/wordpress.org\/plugins\/oats-africa-event-data-migrator\/","author":23514595,"comment_status":"closed","ping_status":"closed","template":"","meta":{"version":"1.0.0","stable_tag":"1.0.0","tested":"7.1.4","requires":"6.0","requires_php":"8.0","requires_plugins":null,"header_name":"Oats Africa Event Data Migrator","header_author":"Oats Africa","header_description":"Connects to another self-hosted WordPress site, pulls its event data (via The Events Calendar, falling back to WooCommerce), normalizes it, and hands it off to an external API you configure. See the \"Privacy\" section of the readme for exactly what data this moves and where.","last_updated":"2026-11-09 19:53:18","external_support_url":"","external_repository_url":"","donate_link":"","header_plugin_uri":"","header_author_uri":"https:\/\/oatsafrica.com\/","rating":0,"author_block_rating":0,"active_installs":0,"downloads":106,"num_ratings":0,"support_threads":0,"support_threads_resolved":0,"author_block_count":0,"sections":["description","installation","faq","changelog"],"tags":{"1.0.0":{"tag":"1.0.0","author":"oatsafrica01","date":"2026-11-09 19:53:18","revision":3793178}},"upgrade_notice":{"1.0.0":"<p>Initial release.<\/p>"},"ratings":[],"assets_icons":[],"assets_banners":[],"assets_blueprints":{},"all_blocks":[],"tagged_versions":["1.0.0"],"block_files":[],"assets_screenshots":[],"screenshots":[]},"plugin_section":[],"plugin_tags":[1556,1486,87,12097,286],"plugin_category":[40,45,59],"plugin_contributors":[269929,269930],"plugin_business_model":[],"class_list":["post-393134","plugin","type-plugin","status-publish","hentry","plugin_tags-api","plugin_tags-events","plugin_tags-import","plugin_tags-the-events-calendar","plugin_tags-woocommerce","plugin_category-calendar-and-events","plugin_category-ecommerce","plugin_category-utilities-and-tools","plugin_contributors-oatsafrica","plugin_contributors-oatsafrica01","plugin_committers-oatsafrica01"],"banners":[],"icons":{"svg":false,"icon":"https:\/\/s.w.org\/plugins\/geopattern-icon\/oats-africa-event-data-migrator.svg","icon_2x":false,"generated":true},"screenshots":[],"raw_content":"<!--section=description-->\n<p>Oats Africa Event Data Migrator connects your WordPress site to <strong>another<\/strong> self-hosted WordPress site \u2014 a source site you already control \u2014 and pulls its event data across so it can be handed off, in a consistent shape, to an external API of your choosing.<\/p>\n\n<p>It's built for the common case where the events \"live\" on one WordPress install (running The Events Calendar, or plain WooCommerce as a fallback), but a separate system needs a normalized feed of that data: another app, an internal dashboard, a data warehouse, anything that can accept a JSON POST.<\/p>\n\n<h4>How it works<\/h4>\n\n<ol>\n<li><strong>Connect.<\/strong> From this plugin's settings screen, point it at your source site's URL and a WordPress <a href=\"https:\/\/make.wordpress.org\/core\/2020\/11\/05\/application-passwords-integration-guide\/\">Application Password<\/a> for a user on that site. The plugin never sees or stores your real account password \u2014 Application Passwords are separate, revocable credentials WordPress core generates for exactly this purpose.<\/li>\n<li><strong>Detect.<\/strong> The plugin checks whether the source site is running <a href=\"https:\/\/wordpress.org\/plugins\/the-events-calendar\/\">The Events Calendar<\/a> and has events in it. If so, it uses TEC's own event, ticket, and attendee data. If not \u2014 or if TEC isn't installed \u2014 it falls back to treating WooCommerce products as events. You can also force either source manually if auto-detection picks the wrong one for your setup.<\/li>\n<li><strong>Import.<\/strong> On demand (there is no automatic background schedule), the plugin fetches everything the detected source has, normalizes it into one consistent format regardless of which source it came from, and hands the whole batch to your configured external API in a single request.<\/li>\n<li><strong>Send.<\/strong> Until you've entered a destination endpoint URL and API key in this plugin's settings, imports run in \"dry run\" mode: nothing is sent anywhere, and the payload that <em>would<\/em> have been sent is saved locally for 24 hours so you can inspect it. Once you configure a real endpoint, imports POST there automatically \u2014 no other setup required.<\/li>\n<\/ol>\n\n<h4>What gets imported<\/h4>\n\n<p>From a <strong>The Events Calendar<\/strong> source: event details (title, description, dates, venue, organizer, cost, categories) in every status visible on your wp-admin Events screen \u2014 Published, Draft, Pending Review, Private, and Scheduled (everything except Trash) \u2014 along with each event's own status, so an import can be reconciled directly against wp-admin instead of only ever importing what's already public; ticket types and prices; and attendee records (name, email, and any custom fields the site's ticket form collects, such as phone number).<\/p>\n\n<p>Every attendee is also matched up with the order behind their ticket, in the same spirit as the events-status handling above \u2014 a complete picture, not just the paid half of it. A ticket bought through WooCommerce is matched to its real WooCommerce order (status, total, currency, billing name\/email\/phone), covering every order status visible on the source site's WooCommerce \u2192 Orders screen (Pending, Processing, On hold, Completed, Cancelled, Refunded, Failed \u2014 everything except Trash), not only completed ones. A free <strong>RSVP<\/strong> \u2014 which has no checkout step and so no real WooCommerce order to find \u2014 instead gets an order built automatically by this plugin: cost <code>0<\/code>, an order id this plugin generates (so it can never collide with a real WooCommerce order number), and a <code>source<\/code> field of <code>rsvp<\/code> (versus <code>woo<\/code> for a real order) so the two are easy to tell apart downstream. Because a free RSVP has no separate \"billing\" step the way a WooCommerce checkout does, its generated order's <code>first_name<\/code> field carries the attendee's whole display name rather than a properly split first name, with <code>last_name<\/code> left empty.<\/p>\n\n<p>From a <strong>WooCommerce<\/strong> fallback source: product details treated as events (best-effort field guessing, since WooCommerce has no built-in \"event\" concept), and the orders placed against each product, in the same <code>tickets<\/code>\/<code>attendees<\/code> shape a The Events Calendar import uses. On a site where The Events Calendar and Event Tickets Plus are also active, some \"products\" are really just ticket SKUs for a TEC event rather than real products \u2014 these are still included rather than silently dropped, but with their event-shaped fields (title, dates, venue) left empty since a ticket product genuinely doesn't carry that information; the ticket's own name and price appear correctly in its <code>tickets<\/code> entry instead, and its buyers still populate <code>attendees<\/code>.<\/p>\n\n<p>An optional \"skip details\" mode is available for faster imports that fetch just the top-level event\/product list, without the extra ticket, attendee, or order data.<\/p>\n\n<p>The admin screen's per-event breakdown and Last Import summary are built to make an import checkable at a glance rather than something you have to take on faith: a Status column on every event, a running total of orders fetched from WooCommerce versus how many are actually linked to an event (split further into paid vs. RSVP-generated), and explicit warnings whenever a fetch fails partway through or a paid order can't be matched \u2014 instead of a partial result that silently looks the same as a complete one.<\/p>\n\n<h4>Requirements<\/h4>\n\n<ul>\n<li>A source WordPress site you have admin access to, reachable over HTTPS, with either The Events Calendar or WooCommerce active.<\/li>\n<li>An external API endpoint that accepts a JSON POST and a bearer token \u2014 this plugin does not include or host that API itself.<\/li>\n<\/ul>\n\n<h3>Privacy<\/h3>\n\n<p>This plugin moves data between two systems you control the configuration of: a source WordPress site, and an external API. Please read this section before activating it on a live site.<\/p>\n\n<p><strong>What is retrieved from the source site:<\/strong> event details, and \u2014 unless you enable \"skip details\" \u2014 ticket, attendee, or order records. Attendee and order records can contain real personal data: names, email addresses, and any custom fields a ticket form asks for (for example, a phone number). Not every order record is retrieved from the source site as-is \u2014 a free RSVP attendee has no real WooCommerce order behind it, so this plugin generates a zero-cost stand-in order for them (see the FAQ above) using the attendee's own name\/email; it is otherwise treated the same as any other order record for privacy purposes. This data is retrieved using the WordPress Application Password you provide, which is scoped to whatever that WordPress user account is permitted to see on the source site.<\/p>\n\n<p><strong>What is sent to the external API:<\/strong> the same normalized data, in a single batch, whenever you click \"Import Now\" and a destination endpoint is configured. There is no background or scheduled sending \u2014 every send is a result of an explicit admin action.<\/p>\n\n<p><strong>What is stored locally, on the site this plugin is installed on:<\/strong>\n* The source site connection details (site URL, username, and an encrypted Application Password).\n* The external API destination (endpoint URL and an encrypted API key).\n* A record of the most recent import (timestamp, counts, success\/failure) for the admin screen's status display.\n* If no API destination is configured yet, the most recent import's full normalized payload \u2014 including any personal data it contains \u2014 saved as a temporary WordPress transient that expires automatically after 24 hours. This exists purely so you can inspect what an import produces before wiring up a real destination.<\/p>\n\n<p><strong>What this plugin does not do:<\/strong> it does not send data to Oats Africa, to any Oats Africa service, or to any third party other than the exact API endpoint you configure yourself. It does not phone home, collect telemetry, or make any outbound request other than to the source site you connect and the API endpoint you set.<\/p>\n\n<p>Because personal data can pass through this plugin, treat both the source site's Application Password and the destination API key with the same care as any other credential, and make sure the destination API itself handles the data you send it appropriately (encryption at rest, access controls, and a retention policy) \u2014 this plugin's job ends once the data leaves your server.<\/p>\n\n<p>Uninstalling the plugin deletes all of the above from your database, including the encrypted credentials and any pending dry-run payload.<\/p>\n\n<!--section=installation-->\n<ol>\n<li>Upload the plugin files to <code>\/wp-content\/plugins\/oats-africa-event-data-migrator<\/code>, or install the plugin through the WordPress admin's Plugins \u2192 Add New screen.<\/li>\n<li>Activate the plugin through the \"Plugins\" screen in WordPress.<\/li>\n<li>Go to the new \"Oats Africa Event Data Migrator\" menu item in your admin sidebar.<\/li>\n<li>Under \"Connection,\" enter your source site's URL, a WordPress username on that site, and an Application Password generated from that user's profile page (Users \u2192 Profile \u2192 Application Passwords).<\/li>\n<li>Click \"Test Connection,\" then \"Detect Source\" to confirm what the plugin found.<\/li>\n<li>Optionally, under \"API Destination,\" enter the endpoint URL and API key for the system that should receive your normalized event data. Leave this blank to run in dry-run mode first.<\/li>\n<li>Click \"Import Now\" to run an import.<\/li>\n<\/ol>\n\n<!--section=faq-->\n<dl>\n<dt id=\"does%20this%20plugin%20send%20my%20data%20anywhere%20by%20default%3F\"><h3>Does this plugin send my data anywhere by default?<\/h3><\/dt>\n<dd><p>No. Nothing leaves your site until you explicitly configure both a source connection (Application Password) and, separately, an external API endpoint and key. Before an API destination is configured, imports run in dry-run mode: the plugin fetches and normalizes data from your source site, but only saves the resulting payload locally (for 24 hours) rather than sending it anywhere.<\/p><\/dd>\n<dt id=\"what%20data%20does%20this%20plugin%20send%20to%20the%20external%20api%3F\"><h3>What data does this plugin send to the external API?<\/h3><\/dt>\n<dd><p>Once configured, it sends whatever the connected source site returns for events, and \u2014 unless \"skip details\" is enabled \u2014 the associated tickets, attendees, or orders. That can include personal data: attendee or customer names, email addresses, and any custom fields a ticket form collects (e.g. phone numbers). See \"Privacy\" below for full details.<\/p><\/dd>\n<dt id=\"where%20are%20my%20credentials%20stored%3F\"><h3>Where are my credentials stored?<\/h3><\/dt>\n<dd><p>Your source site's Application Password and your external API key are encrypted before being saved to the WordPress database, using a key derived from your site's own WordPress secret keys (the ones already in <code>wp-config.php<\/code>). They are never displayed back in the admin screen once saved, and are deleted entirely when the plugin is uninstalled.<\/p><\/dd>\n<dt id=\"does%20this%20work%20with%20any%20wordpress%20site%20as%20a%20source%2C%20or%20does%20the%20source%20site%20need%20this%20plugin%20too%3F\"><h3>Does this work with any WordPress site as a source, or does the source site need this plugin too?<\/h3><\/dt>\n<dd><p>The source site does not need this plugin installed. It only needs The Events Calendar or WooCommerce active, and its REST API reachable (the WordPress REST API is enabled by default), plus an Application Password for an account with permission to read the relevant data.<\/p><\/dd>\n<dt id=\"what%20happens%20if%20the%20source%20site%20has%20neither%20the%20events%20calendar%20nor%20woocommerce%3F\"><h3>What happens if the source site has neither The Events Calendar nor WooCommerce?<\/h3><\/dt>\n<dd><p>Detection reports that no supported source was found, and imports won't run until one of those is active on the source site.<\/p><\/dd>\n<dt id=\"why%20does%20an%20order%20show%20up%20with%20a%20status%20of%20%22rsvp%22%20and%20a%20cost%20of%200%20%E2%80%94%20i%20can%27t%20find%20it%20on%20the%20woocommerce%20orders%20screen\"><h3>Why does an order show up with a status of \"rsvp\" and a cost of 0 \u2014 I can't find it on the WooCommerce Orders screen<\/h3><\/dt>\n<dd><p>That's expected, not a bug. A free RSVP has no checkout step, so there's no real WooCommerce order behind it to fetch \u2014 this plugin builds one automatically instead, so the attendee still has an order on record on the receiving side, rather than being the one type of attendee with nothing associated with them. Its <code>source<\/code> field reads <code>rsvp<\/code> (a real order's reads <code>woo<\/code>) specifically so it's easy to tell the two apart; there's nothing to go looking for on the source site's Orders screen, because it was never a WooCommerce order in the first place.<\/p><\/dd>\n\n<\/dl>\n\n<!--section=changelog-->\n<h4>1.0.0<\/h4>\n\n<ul>\n<li>Initial public release.<\/li>\n<li>Detects The Events Calendar or WooCommerce automatically on a connected source site, with a manual override.<\/li>\n<li>Normalizes events, tickets\/attendees (TEC) or products\/orders (WooCommerce) into one consistent shape.<\/li>\n<li>Events are fetched in every status visible on wp-admin (Published, Draft, Pending Review, Private, Scheduled \u2014 everything except Trash), each carrying its own status, so an import can be checked directly against wp-admin instead of only ever covering what's already public.<\/li>\n<li>WooCommerce orders are fetched across every order status visible on the Orders screen (Pending, Processing, On hold, Completed, Cancelled, Refunded, Failed \u2014 everything except Trash), explicitly, rather than relying on a single default status.<\/li>\n<li>A free RSVP attendee (no checkout, so no real WooCommerce order exists) gets an order built automatically \u2014 cost 0, a generated order id, and a <code>source<\/code> of <code>rsvp<\/code> versus <code>woo<\/code> for a real order \u2014 so every attendee has an order on record rather than the RSVP ones having nothing associated with them.<\/li>\n<li>A WooCommerce order that looks like it belongs to a paid ticket but can't be matched to anything fetched is surfaced as an explicit warning, instead of silently looking identical to a normal RSVP (which has no real order to find, by design).<\/li>\n<li>A fetch failure partway through the WooCommerce orders, tickets, or attendees catalogs surfaces as an explicit warning on the admin screen, instead of silently producing a smaller-than-real result with no indication anything went wrong.<\/li>\n<li>Admin screen: a per-event breakdown table shows each event's status and ticket\/attendee\/order counts, with notes explaining an incomplete\/undated event's absence and how RSVP-generated orders work.<\/li>\n<li>Admin screen: the Last Import summary reports total orders fetched from WooCommerce, how many of those are linked to an event, and how that splits between paid and RSVP-generated.<\/li>\n<li>Sends imports to an admin-configured external API, with a dry-run mode until a destination is set.<\/li>\n<li>Optional \"skip details\" mode for faster, events-only imports.<\/li>\n<\/ul>","raw_excerpt":"Pulls event, ticket, and attendee data from another self-hosted WordPress site and sends it to an external API you configure.","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin\/393134","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin"}],"about":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/types\/plugin"}],"replies":[{"embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/comments?post=393134"}],"author":[{"embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wporg\/v1\/users\/oatsafrica01"}],"wp:attachment":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/media?parent=393134"}],"wp:term":[{"taxonomy":"plugin_section","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_section?post=393134"},{"taxonomy":"plugin_tags","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_tags?post=393134"},{"taxonomy":"plugin_category","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_category?post=393134"},{"taxonomy":"plugin_contributors","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_contributors?post=393134"},{"taxonomy":"plugin_business_model","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_business_model?post=393134"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}