Building a custom integration with gift registries, or changing how registries behave on your store? SaveTo Wishlist Registry adds PHP hooks and REST API routes that let you do both without editing plugin files. This article lists the routes, who can call them, what the public routes return, and the hooks you can use, with working examples.
For the developer reference of the base plugin, see How to extend SaveTo Wishlist with hooks and the REST API.
Before you start
- SaveTo Wishlist Registry (listed in WordPress as SaveTo Wishlist Gift Registry Addon) needs WooCommerce and SaveTo Wishlist Lite to be active. It comes with the Plus and Business plans and has its own license key, separate from SaveTo Wishlist Pro.
- Every hook name starts with
stwreg_. Add your code to a small plugin or to a child themefunctions.phpfile. - The hooks and routes in this article exist in SaveTo Wishlist Registry 1.0.0.
- Registry data lives in custom tables that use your WordPress table prefix:
wp_stw_registries,wp_stw_registry_items,wp_stw_registry_purchasesandwp_stw_registry_contributions. Use the hooks and routes below instead of writing to these tables directly.
How the REST API works
All routes use the namespace saveto-wishlist-registry/v1. On a site at https://example.com, the full base URL is https://example.com/wp-json/saveto-wishlist-registry/v1/.
Routes that need a logged-in user use standard WordPress REST authentication. In a browser session, send the X-WP-Nonce header with a wp_rest nonce. For server-to-server calls, use a WordPress application password. Public routes need no login.
The “Who can call it” column in the route tables uses these terms:
- Anyone: no login needed.
- Logged-in user: any logged-in account.
- Registry owner: the logged-in user who owns the registry in the route, or any user with the
manage_woocommercecapability (shop managers and administrators by default). - Purchase owner: the logged-in user who owns the registry that the purchase belongs to, or any user with the
manage_woocommercecapability. - Owner or shop manager (registry_id): the owner of the registry given in the
registry_idquery parameter, or a user with themanage_woocommercecapability. Ifregistry_idis missing, only users with that capability pass. - Shop manager: a user with the
manage_woocommercecapability.
Errors come back as standard WordPress REST errors with a code, a message, and an HTTP status. Rate-limited requests return status 429 with the code rate_limited.
REST API routes
Public routes
| Method | Route | Who can call it | What it does |
|---|---|---|---|
| GET | /registry/search | Anyone | Searches published registries. Query parameters: name (at least 2 characters), event_type, page, per_page (maximum 20, default 20). Limited to 30 requests per 5 minutes for each IP address. |
| GET | /registry/{url_code} | Anyone | Returns one registry by its public code. The code is 4 to 16 letters or numbers. Returns 404 for a deleted registry. Archived registries are still returned. |
| GET | /registry/{url_code}/items | Anyone | Returns the items in a registry. Returns 404 for a deleted registry. |
| POST | /contributions | Anyone | Records a pending money contribution. Body fields: registry_id, amount, registry_item_id (optional), contributor_name, contributor_email, message. The registry must be published and have fund pooling turned on. Returns the new id and the status pending. |
The POST /contributions route only records a row with the status pending. It does not take a payment. A payment integration must update the status itself.
Registry owner routes
| Method | Route | Who can call it | What it does |
|---|---|---|---|
| POST | /registry/convert | Logged-in user | Converts a wishlist into a registry for the current user. Needs an active license, otherwise it returns 403 with the code license_inactive. Body fields: collection_id, event_type, event_date, registrant_name, co_registrant_name, message, location, cover_image_attachment_id, allow_fund_pooling. |
| POST | /registry/cover-image | Logged-in user | Uploads a cover image as a multipart file in the file field. JPEG, PNG or WebP, up to 5 MB. Returns attachment_id and url. |
| GET | /my-registries | Logged-in user | Lists only the registries owned by the current user, including shop managers, with item counts, thank-you counts, and an overall stats block. |
| PUT | /registry/{id} | Registry owner | Updates registry details. Send a JSON body with the fields to change. |
| DELETE | /registry/{id} | Registry owner | Deletes the registry. Returns {"deleted":true}. |
| POST | /registry/{id}/items | Registry owner | Adds a product to the registry. Body fields: product_id (required), variation_id, quantity (default 1), priority (default medium; allowed values are low, medium and high). |
| PATCH | /registry/{id}/items/reorder | Registry owner | Saves a new item order. Body field: item_ids (required array of item IDs). Every ID must belong to the registry. |
| PATCH | /registry/{id}/items/{item_id} | Registry owner | Changes the requested quantity of one item. Body field: quantity. The quantity cannot go below the number already bought. Returns 409 if the registry is archived. |
| DELETE | /registry/{id}/items/{item_id} | Registry owner | Removes one item. Returns 409 if the registry is archived. |
| GET | /registry/{id}/products/search | Registry owner | Searches products to add. Query parameters: q, per_page. Limited to 60 requests per 5 minutes for each user. |
| GET | /registry/{id}/products/{product_id}/variations | Registry owner | Lists the variations of a product. Limited to 60 requests per 5 minutes for each user. |
Purchase and contribution routes
| Method | Route | Who can call it | What it does |
|---|---|---|---|
| GET | /purchases | Owner or shop manager (registry_id) | Lists purchases for a registry. Query parameters: registry_id, thank_you_sent (all, true or false), page, per_page, orderby, order. Each row has the buyer name and email, so only owners and shop managers can read it. |
| POST | /purchases/{id}/note | Purchase owner | Saves the owner note on a purchase. Body field: note. |
| POST | /purchases/{id}/thank-you | Purchase owner | Marks one purchase as thanked or not. Body field: sent. |
| POST | /purchases/bulk-thank-you | Purchase owner | Marks several purchases at once. Body fields: ids and sent. For an owner, all IDs must belong to registries that they own. |
| GET | /contributions | Owner or shop manager (registry_id) | Lists the contributions for the registry in the registry_id query parameter, newest first. Rows include the contributor email. |
Registry admin routes
All admin routes need a shop manager.
| Method | Route | What it does |
|---|---|---|
| GET | /admin/registries | Lists registries. Query parameters: status, event_type, search, orderby, order, page, per_page, include_deleted. |
| GET | /admin/registries/{id} | Returns one registry with its items and up to 100 purchases. |
| PUT | /admin/registries/{id} | Updates a registry as an admin. |
| DELETE | /admin/registries/{id} | Deletes a registry as an admin. |
| POST | /admin/registries/{id}/close | Archives a registry. The public page stays online with an archived banner. |
| POST | /admin/registries/{id}/restore | Restores a registry. |
| POST | /admin/registries/bulk | Runs one action on many registries. Body fields: action (archive, unarchive or delete) and ids. Returns a result for each ID plus succeeded and failed counts. |
| POST | /admin/registries/convert | Converts a wishlist into a registry for the wishlist owner. Required body field: collection_id. Other fields match /registry/convert. |
| GET | /admin/registries/by-collections | Finds the registry for each wishlist ID in the ids query parameter (comma-separated). Returns registry_id, url_code, status and public_url for each match. Deleted registries are left out. |
What the public routes expose
Public routes can be called by anyone, so check what they return before you build on them.
GET /registry/{url_code} returns the registry record and the landing page data merged into one response. It includes:
- Registry fields:
id,collection_id,owner_user_id,url_code,event_type,event_date,registrant_name,co_registrant_name,message,location,cover_image_id,allow_fund_pooling,status,previous_url_code,created_atandupdated_at. - Display fields:
title,event_type_label,event_date_iso,event_date_formatted,registrant_names,cover_image_url,cover_image_alt,is_archived,is_ownerandpublic_url. - An
itemslist. Each item has the product title, product URL, image, formatted price,quantityandpurchased_quantity.
When the caller owns the registry or is a shop manager, the response also has an event_types list.
GET /registry/{url_code}/items returns each item with its ID, a product block (ID, variation ID, name, SKU, thumbnail URL, formatted price, and whether the product still exists), quantity_wanted, quantity_purchased, quantity_remaining, is_fully_claimed, is_archived and priority.
GET /registry/search returns only published registries. Each result has id, url_code, event_type, event_date, registrant_name, co_registrant_name, cover_image_id, public_url and event_date_formatted. The response also has total, page and per_page.
None of the public routes return buyer names, buyer emails, purchase rows, order IDs, or contribution rows. Those are only available through the owner and admin routes. Use the stwreg_registry_data filter below to add or remove fields from the GET /registry/{url_code} response. Test your registry page after you remove a field.
Hooks
The hooks below are the ones most useful for extending the plugin. Argument counts are exact. When you add a hook with more than one argument, pass the argument count as the fourth parameter of add_filter() or add_action().
Filters for registry data and the landing page
| Filter | Arguments | What it changes |
|---|---|---|
stwreg_registry_data | $data (array), $row (object) | The full response of GET /registry/{url_code}. $row is the raw registry row. |
stwreg_registry_items_data | $response (array), $row (object) | The response of GET /registry/{url_code}/items. The array has data, total, currentPage and is_archived. |
stwreg_registry_landing_data | $payload (array), $row (object) | The data used to build the landing page and the REST response, before it is rendered. |
stwreg_search_results | $results (array), $args (array) | The registry search result before it is returned. The array has data, total, page and per_page. |
stwreg_landing_html | $html (string), $payload (array) | The final HTML of the whole registry landing page. |
stwreg_landing_item_card | $html (string), $item (array), $payload (array) | The HTML of one item card on the landing page. |
Always return the same type that you receive. The plugin casts the return value to an array or a string.
This example adds a custom field to the public registry response:
add_filter( 'stwreg_registry_data', function ( array $data, $row ) {
$data['shipping_note'] = 'Gifts ship within 3 business days.';
return $data;
}, 10, 2 );
This example adds a line of text under the landing page:
add_filter( 'stwreg_landing_html', function ( $html, $payload ) {
if ( ! empty( $payload['is_archived'] ) ) {
return $html;
}
return $html . '<p>' . esc_html__( 'Need help choosing a gift? Contact us.', 'your-text-domain' ) . '</p>';
}, 10, 2 );
The landing page is also built from smaller section filters, one for each part of the page. Their names start with stwreg_landing_section_ and they take $html and $payload. Examples are stwreg_landing_section_header, stwreg_landing_section_message and stwreg_landing_section_footer. To change the look of the page without code, see Registry landing page appearance settings.
Rate limit filters
The routes below limit how often one user or one IP address can call them. Each limit has two filters. One sets the number of requests. The other sets the window in seconds. Each filter takes one argument and must return an integer.
| Limit filter | Window filter | Default | Route | Counted per |
|---|---|---|---|---|
stwreg_add_item_rate_limit | stwreg_add_item_rate_window | 30 requests in 5 minutes | POST /registry/{id}/items | User |
stwreg_cover_image_rate_limit | stwreg_cover_image_rate_window | 30 requests in 5 minutes | POST /registry/cover-image | User |
stwreg_convert_rate_limit | stwreg_convert_rate_window | 5 requests in 5 minutes | POST /registry/convert | User |
stwreg_contribute_rate_limit | stwreg_contribute_rate_window | 10 requests in 5 minutes | POST /contributions | IP address and registry |
The search limit (30 requests in 5 minutes for each IP address) and the product search and variation limits (60 requests in 5 minutes for each user) have no filter. The contribution amount limit has its own filter, stwreg_contribution_max_amount (float, default 100000).
This example raises the add-item limit to 60 requests in 10 minutes:
add_filter( 'stwreg_add_item_rate_limit', function ( $limit ) {
return 60;
} );
add_filter( 'stwreg_add_item_rate_window', function ( $window ) {
return 10 * MINUTE_IN_SECONDS;
} );
By default, the plugin uses REMOTE_ADDR to find the visitor IP address for the IP-based limits. If your site is behind a proxy or CDN, REMOTE_ADDR is the proxy address, so all visitors share one limit. Use the stwreg_trusted_ip_headers filter to list the server variables that hold the real IP address. Only list headers that your proxy sets, because visitors can fake the others.
add_filter( 'stwreg_trusted_ip_headers', function ( array $headers ) {
$headers[] = 'HTTP_CF_CONNECTING_IP';
return $headers;
} );
Purchase actions
| Action | Arguments | When it runs |
|---|---|---|
stwreg_after_save_purchase | $purchase_id (int), $payload (array) | After a purchase row is created or updated. It runs for new rows and for updates to an existing order line. |
stwreg_after_void_purchase | $purchase_id (int), $purchase (object), $previous_status (string), $refunded_quantity (int) | When a purchase moves into the voided state, for example after a refund. |
stwreg_after_record_purchases_from_order | $order_id (int), $order (WC_Order) | After the plugin processes a paid order that contains registry items. It runs even if some items could not be matched. |
stwreg_overpurchase_recorded | $registry_item_id (int), $purchased_total (int), $requested_quantity (int) | After the total of recorded purchases for an item is higher than the quantity the owner asked for. |
The $payload array of stwreg_after_save_purchase has these keys: registry_id, registry_item_id, order_id, line_item_id, quantity, buyer_name, buyer_email and status. The status is recorded, voided or refunded. The plugin saves purchases from orders, and it saves them again when the same order line is processed again. Write your handler so that it is safe to run twice for the same purchase.
add_action( 'stwreg_after_save_purchase', function ( $purchase_id, $payload ) {
if ( 'recorded' !== $payload['status'] ) {
return;
}
error_log( sprintf(
'Registry %d: item %d bought (purchase %d).',
$payload['registry_id'],
$payload['registry_item_id'],
$purchase_id
) );
}, 10, 2 );
A registry item can end up with more recorded purchases than the owner asked for, for example when two buyers check out at the same time. The plugin allows this by design. When it happens, it runs stwreg_overpurchase_recorded after it saves the new total. The action can run again each time the plugin recalculates an item that is still over its quantity, so do not assume it runs only once.
add_action( 'stwreg_overpurchase_recorded', function ( $registry_item_id, $purchased_total, $requested_quantity ) {
error_log( sprintf(
'Registry item %d: %d bought, %d requested.',
$registry_item_id,
$purchased_total,
$requested_quantity
) );
}, 10, 3 );
Email filters and actions
The plugin sends three emails: the owner purchase email (type owner_new_purchase), the owner refund email (type owner_order_refund), and the admin new registry email (type admin_new_registry). To change what they say, see How to set up gift registry email notifications.
| Hook | Type | Arguments | What it does |
|---|---|---|---|
stwreg_email_send_async | Filter | $async (bool), $payload (array) | Default true. When true, the plugin queues the email with WP-Cron. When false, it sends the email in the same request. |
stwreg_email_{type}_subject | Filter | $subject (string), $context (array), $payload (array) | Changes the subject. Replace {type} with an email type, for example stwreg_email_owner_new_purchase_subject. |
stwreg_email_{type}_body | Filter | $body (string), $context (array), $payload (array) | Changes the rendered body before the email layout is added. |
stwreg_after_email_dispatched | Action | $type (string), $payload (array), $success (bool) | Runs after the plugin tries to send an email. $success is false when the email was skipped or failed. |
Use the synchronous mode when WP-Cron is turned off on your site or when you want to test an email in a single request:
add_filter( 'stwreg_email_send_async', '__return_false' );
If WP-Cron is turned off and you leave the default, queued emails wait until a real cron job runs.
This example changes the subject of the owner purchase email:
add_filter( 'stwreg_email_owner_new_purchase_subject', function ( $subject, $context, $payload ) {
return 'Good news: ' . $subject;
}, 10, 3 );
For the owner emails, $context includes recipient, owner_display_name, registry (row object), purchase (row object), item (row object), subject, owner_dashboard and public_url.
REST permission filters
Every route runs a permission check, and each check has a filter. Use these filters to make access stricter. They cannot make access wider. If the plugin denies a request, the filter cannot allow it. If the plugin allows a request, the filter can still return a WP_Error to deny it.
| Filter | Used by | Arguments |
|---|---|---|
stwreg_api_public_permissions_check | Public routes (/registry/search, /registry/{url_code}, /registry/{url_code}/items, POST /contributions) | $decision (bool or WP_Error), $request (WP_REST_Request) |
stwreg_api_logged_in_permissions_check | Routes for any logged-in user | $decision, $request |
stwreg_api_owner_permissions_check | Registry owner routes | $decision, $request |
stwreg_api_admin_permissions_check | Registry admin routes | $decision, $request |
This example makes the registry search available to logged-in users only:
add_filter( 'stwreg_api_public_permissions_check', function ( $decision, $request ) {
$search_route = '/saveto-wishlist-registry/v1/registry/search';
if ( $search_route === $request->get_route() && ! is_user_logged_in() ) {
return new WP_Error(
'rest_forbidden',
'Log in to search registries.',
array( 'status' => 401 )
);
}
return $decision;
}, 10, 2 );
The routes /purchases/{id}/note, /purchases/{id}/thank-you and /purchases/bulk-thank-you use their own ownership checks and do not run these filters. GET /purchases and GET /contributions let the registry owner through without a filter. For everyone else they run the admin check, so stwreg_api_admin_permissions_check applies to them.
Other actions
| Action | Arguments | When it runs |
|---|---|---|
stwreg_before_convert_wishlist_to_registry | $collection_id (int), $args (array) | Before a wishlist is converted. See How to convert a wishlist into a gift registry. |
stwreg_after_convert_wishlist_to_registry | $registry_id (int), $collection_id (int), $args (array) | After a wishlist is converted. |
stwreg_after_add_registry_item | $registry_id (int), $row (object) | After an item is added to a registry. |
stwreg_after_save_contribution | $contribution_id (int), $registry_id (int) | After a contribution row is saved. |
Frequently asked questions
Can I change the registry page URL prefix?
Yes. The stwreg_url_prefix filter takes the prefix as a string, and stwreg_registry_public_url takes the full URL (string) and the public code (string).
Where do I find privacy export and erase for registry data?
See How to export and erase registry data for privacy requests.
What’s next?
- How to view gift purchases and send thank-you notes
- How to set up a Find a Registry search page
- How to set up gift registry email notifications
Need help?
If you have a question or run into any issues, we’re here to help.
- Premium users: Open a support ticket
- Free users: Visit our community forum
