class Akismet_REST_API { /** * Register the REST API routes. */ public static function init() { if ( ! function_exists( 'register_rest_route' ) ) { // The REST API wasn't integrated into core until 4.4, and we support 4.0+ (for now). return false; } register_rest_route( 'akismet/v1', '/key', array( array( 'methods' => WP_REST_Server::READABLE, 'permission_callback' => array( 'Akismet_REST_API', 'privileged_permission_callback' ), 'callback' => array( 'Akismet_REST_API', 'get_key' ), ), array( 'methods' => WP_REST_Server::EDITABLE, 'permission_callback' => array( 'Akismet_REST_API', 'privileged_permission_callback' ), 'callback' => array( 'Akismet_REST_API', 'set_key' ), 'args' => array( 'key' => array( 'required' => true, 'type' => 'string', 'sanitize_callback' => array( 'Akismet_REST_API', 'sanitize_key' ), 'description' => __( 'A 12-character Akismet API key. Available at akismet.com/get/', 'akismet' ), ), ), ), array( 'methods' => WP_REST_Server::DELETABLE, 'permission_callback' => array( 'Akismet_REST_API', 'privileged_permission_callback' ), 'callback' => array( 'Akismet_REST_API', 'delete_key' ), ) ) ); register_rest_route( 'akismet/v1', '/settings/', array( array( 'methods' => WP_REST_Server::READABLE, 'permission_callback' => array( 'Akismet_REST_API', 'privileged_permission_callback' ), 'callback' => array( 'Akismet_REST_API', 'get_settings' ), ), array( 'methods' => WP_REST_Server::EDITABLE, 'permission_callback' => array( 'Akismet_REST_API', 'privileged_permission_callback' ), 'callback' => array( 'Akismet_REST_API', 'set_boolean_settings' ), 'args' => array( 'akismet_strictness' => array( 'required' => false, 'type' => 'boolean', 'description' => __( 'If true, Akismet will automatically discard the worst spam automatically rather than putting it in the spam folder.', 'akismet' ), ), 'akismet_show_user_comments_approved' => array( 'required' => false, 'type' => 'boolean', 'description' => __( 'If true, show the number of approved comments beside each comment author in the comments list page.', 'akismet' ), ), ), ) ) ); register_rest_route( 'akismet/v1', '/stats', array( 'methods' => WP_REST_Server::READABLE, 'permission_callback' => array( 'Akismet_REST_API', 'privileged_permission_callback' ), 'callback' => array( 'Akismet_REST_API', 'get_stats' ), 'args' => array( 'interval' => array( 'required' => false, 'type' => 'string', 'sanitize_callback' => array( 'Akismet_REST_API', 'sanitize_interval' ), 'description' => __( 'The time period for which to retrieve stats. Options: 60-days, 6-months, all', 'akismet' ), 'default' => 'all', ), ), ) ); register_rest_route( 'akismet/v1', '/stats/(?P[\w+])', array( 'args' => array( 'interval' => array( 'description' => __( 'The time period for which to retrieve stats. Options: 60-days, 6-months, all', 'akismet' ), 'type' => 'string', ), ), array( 'methods' => WP_REST_Server::READABLE, 'permission_callback' => array( 'Akismet_REST_API', 'privileged_permission_callback' ), 'callback' => array( 'Akismet_REST_API', 'get_stats' ), ) ) ); register_rest_route( 'akismet/v1', '/alert', array( array( 'methods' => WP_REST_Server::READABLE, 'permission_callback' => array( 'Akismet_REST_API', 'remote_call_permission_callback' ), 'callback' => array( 'Akismet_REST_API', 'get_alert' ), 'args' => array( 'key' => array( 'required' => false, 'type' => 'string', 'sanitize_callback' => array( 'Akismet_REST_API', 'sanitize_key' ), 'description' => __( 'A 12-character Akismet API key. Available at akismet.com/get/', 'akismet' ), ), ), ), array( 'methods' => WP_REST_Server::EDITABLE, 'permission_callback' => array( 'Akismet_REST_API', 'remote_call_permission_callback' ), 'callback' => array( 'Akismet_REST_API', 'set_alert' ), 'args' => array( 'key' => array( 'required' => false, 'type' => 'string', 'sanitize_callback' => array( 'Akismet_REST_API', 'sanitize_key' ), 'description' => __( 'A 12-character Akismet API key. Available at akismet.com/get/', 'akismet' ), ), ), ), array( 'methods' => WP_REST_Server::DELETABLE, 'permission_callback' => array( 'Akismet_REST_API', 'remote_call_permission_callback' ), 'callback' => array( 'Akismet_REST_API', 'delete_alert' ), 'args' => array( 'key' => array( 'required' => false, 'type' => 'string', 'sanitize_callback' => array( 'Akismet_REST_API', 'sanitize_key' ), 'description' => __( 'A 12-character Akismet API key. Available at akismet.com/get/', 'akismet' ), ), ), ) ) ); register_rest_route( 'akismet/v1', '/webhook', array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( 'Akismet_REST_API', 'receive_webhook' ), 'permission_callback' => array( 'Akismet_REST_API', 'remote_call_permission_callback' ), ) ); } /** * Get the current Akismet API key. * * @param WP_REST_Request $request * @return WP_Error|WP_REST_Response */ public static function get_key( $request = null ) { return rest_ensure_response( Akismet::get_api_key() ); } /** * Set the API key, if possible. * * @param WP_REST_Request $request * @return WP_Error|WP_REST_Response */ public static function set_key( $request ) { if ( defined( 'WPCOM_API_KEY' ) ) { return rest_ensure_response( new WP_Error( 'hardcoded_key', __( 'This site\'s API key is hardcoded and cannot be changed via the API.', 'akismet' ), array( 'status'=> 409 ) ) ); } $new_api_key = $request->get_param( 'key' ); if ( ! self::key_is_valid( $new_api_key ) ) { return rest_ensure_response( new WP_Error( 'invalid_key', __( 'The value provided is not a valid and registered API key.', 'akismet' ), array( 'status' => 400 ) ) ); } update_option( 'wordpress_api_key', $new_api_key ); return self::get_key(); } /** * Unset the API key, if possible. * * @param WP_REST_Request $request * @return WP_Error|WP_REST_Response */ public static function delete_key( $request ) { if ( defined( 'WPCOM_API_KEY' ) ) { return rest_ensure_response( new WP_Error( 'hardcoded_key', __( 'This site\'s API key is hardcoded and cannot be deleted.', 'akismet' ), array( 'status'=> 409 ) ) ); } delete_option( 'wordpress_api_key' ); return rest_ensure_response( true ); } /** * Get the Akismet settings. * * @param WP_REST_Request $request * @return WP_Error|WP_REST_Response */ public static function get_settings( $request = null ) { return rest_ensure_response( array( 'akismet_strictness' => ( get_option( 'akismet_strictness', '1' ) === '1' ), 'akismet_show_user_comments_approved' => ( get_option( 'akismet_show_user_comments_approved', '1' ) === '1' ), ) ); } /** * Update the Akismet settings. * * @param WP_REST_Request $request * @return WP_Error|WP_REST_Response */ public static function set_boolean_settings( $request ) { foreach ( array( 'akismet_strictness', 'akismet_show_user_comments_approved', ) as $setting_key ) { $setting_value = $request->get_param( $setting_key ); if ( is_null( $setting_value ) ) { // This setting was not specified. continue; } // From 4.7+, WP core will ensure that these are always boolean // values because they are registered with 'type' => 'boolean', // but we need to do this ourselves for prior versions. $setting_value = Akismet_REST_API::parse_boolean( $setting_value ); update_option( $setting_key, $setting_value ? '1' : '0' ); } return self::get_settings(); } /** * Parse a numeric or string boolean value into a boolean. * * @param mixed $value The value to convert into a boolean. * @return bool The converted value. */ public static function parse_boolean( $value ) { switch ( $value ) { case true: case 'true': case '1': case 1: return true; case false: case 'false': case '0': case 0: return false; default: return (bool) $value; } } /** * Get the Akismet stats for a given time period. * * Possible `interval` values: * - all * - 60-days * - 6-months * * @param WP_REST_Request $request * @return WP_Error|WP_REST_Response */ public static function get_stats( $request ) { $api_key = Akismet::get_api_key(); $interval = $request->get_param( 'interval' ); $stat_totals = array(); $request_args = array( 'blog' => get_option( 'home' ), 'key' => $api_key, 'from' => $interval, ); $request_args = apply_filters( 'akismet_request_args', $request_args, 'get-stats' ); $response = Akismet::http_post( Akismet::build_query( $request_args ), 'get-stats' ); if ( ! empty( $response[1] ) ) { $stat_totals[$interval] = json_decode( $response[1] ); } return rest_ensure_response( $stat_totals ); } /** * Get the current alert code and message. Alert codes are used to notify the site owner * if there's a problem, like a connection issue between their site and the Akismet API, * invalid requests being sent, etc. * * @param WP_REST_Request $request * @return WP_Error|WP_REST_Response */ public static function get_alert( $request ) { return rest_ensure_response( array( 'code' => get_option( 'akismet_alert_code' ), 'message' => get_option( 'akismet_alert_msg' ), ) ); } /** * Update the current alert code and message by triggering a call to the Akismet server. * * @param WP_REST_Request $request * @return WP_Error|WP_REST_Response */ public static function set_alert( $request ) { delete_option( 'akismet_alert_code' ); delete_option( 'akismet_alert_msg' ); // Make a request so the most recent alert code and message are retrieved. Akismet::verify_key( Akismet::get_api_key() ); return self::get_alert( $request ); } /** * Clear the current alert code and message. * * @param WP_REST_Request $request * @return WP_Error|WP_REST_Response */ public static function delete_alert( $request ) { delete_option( 'akismet_alert_code' ); delete_option( 'akismet_alert_msg' ); return self::get_alert( $request ); } private static function key_is_valid( $key ) { $request_args = array( 'key' => $key, 'blog' => get_option( 'home' ), ); $request_args = apply_filters( 'akismet_request_args', $request_args, 'verify-key' ); $response = Akismet::http_post( Akismet::build_query( $request_args ), 'verify-key' ); if ( $response[1] == 'valid' ) { return true; } return false; } public static function privileged_permission_callback() { return current_user_can( 'manage_options' ); } /** * For calls that Akismet.com makes to the site to clear outdated alert codes, use the API key for authorization. */ public static function remote_call_permission_callback( $request ) { $local_key = Akismet::get_api_key(); return $local_key && ( strtolower( $request->get_param( 'key' ) ) === strtolower( $local_key ) ); } public static function sanitize_interval( $interval, $request, $param ) { $interval = trim( $interval ); $valid_intervals = array( '60-days', '6-months', 'all', ); if ( ! in_array( $interval, $valid_intervals ) ) { $interval = 'all'; } return $interval; } public static function sanitize_key( $key, $request, $param ) { return trim( $key ); } /** * Process a webhook request from the Akismet servers. * * @param WP_REST_Request $request * @return WP_Error|WP_REST_Response */ public static function receive_webhook( $request ) { Akismet::log( array( 'Webhook request received', $request->get_body() ) ); /** * The request body should look like this: * array( * 'key' => '1234567890abcd', * 'endpoint' => '[comment-check|submit-ham|submit-spam]', * 'comments' => array( * array( * 'guid' => '[...]', * 'result' => '[true|false]', * 'comment_author' => '[...]', * [...] * ), * array( * 'guid' => '[...]', * [...], * ), * [...] * ) * ) * * Multiple comments can be included in each request, and the only truly required * field for each is the guid, although it would be friendly to include also * comment_post_ID, comment_parent, and comment_author_email, if possible to make * searching easier. */ // The response will include statuses for the result of each comment that was supplied. $response = array( 'comments' => array(), ); $endpoint = $request->get_param( 'endpoint' ); switch ( $endpoint ) { case 'comment-check': $webhook_comments = $request->get_param( 'comments' ); if ( ! is_array( $webhook_comments ) ) { return rest_ensure_response( new WP_Error( 'malformed_request', __( 'The \'comments\' parameter must be an array.', 'akismet' ), array( 'status' => 400 ) ) ); } foreach ( $webhook_comments as $webhook_comment ) { $guid = $webhook_comment['guid']; if ( ! $guid ) { // Without the GUID, we can't be sure that we're matching the right comment. // We'll make it a rule that any comment without a GUID is ignored intentionally. continue; } // Search on the fields that are indexed in the comments table, plus the GUID. // The GUID is the only thing we really need to search on, but comment_meta // is not indexed in a useful way if there are many many comments. This // should help narrow it down first. $queryable_fields = array( 'comment_post_ID' => 'post_id', 'comment_parent' => 'parent', 'comment_author_email' => 'author_email', ); $query_args = array(); $query_args['status'] = 'any'; $query_args['meta_key'] = 'akismet_guid'; $query_args['meta_value'] = $guid; foreach ( $queryable_fields as $queryable_field => $wp_comment_query_field ) { if ( isset( $webhook_comment[ $queryable_field ] ) ) { $query_args[ $wp_comment_query_field ] = $webhook_comment[ $queryable_field ]; } } $comments_query = new WP_Comment_Query( $query_args ); $comments = $comments_query->comments; if ( ! $comments ) { // Unexpected, although the comment could have been deleted since being submitted. Akismet::log( 'Webhook failed: no matching comment found.' ); $response['comments'][ $guid ] = array( 'status' => 'error', 'message' => __( 'Could not find matching comment.', 'akismet' ) ); continue; } if ( count( $comments ) > 1 ) { // Two comments shouldn't be able to match the same GUID. Akismet::log( 'Webhook failed: multiple matching comments found.', $comments ); $response['comments'][ $guid ] = array( 'status' => 'error', 'message' => __( 'Multiple comments matched request.', 'akismet' ) ); continue; } else { // We have one single match, as hoped for. Akismet::log( 'Found matching comment.', $comments ); $current_status = wp_get_comment_status( $comments[0] ); $result = $webhook_comment['result']; if ( 'true' == $result ) { Akismet::log( 'Comment should be spam' ); // The comment should be classified as spam. if ( 'spam' != $current_status ) { // The comment is not classified as spam. If Akismet was the one to act on it, move it to spam. if ( Akismet::last_comment_status_change_came_from_akismet( $comments[0]->comment_ID ) ) { Akismet::log( 'Comment is not spam; marking as spam.' ); wp_spam_comment( $comments[0] ); Akismet::update_comment_history( $comments[0]->comment_ID, '', 'webhook-spam' ); } else { Akismet::log( 'Comment is not spam, but it has already been manually handled by some other process.' ); Akismet::update_comment_history( $comments[0]->comment_ID, '', 'webhook-spam-noaction' ); } } } else if ( 'false' == $result ) { Akismet::log( 'Comment should be ham' ); // The comment should be classified as ham. if ( 'spam' == $current_status ) { Akismet::log( 'Comment is spam.' ); // The comment is classified as spam. If Akismet was the one to label it as spam, unspam it. if ( Akismet::last_comment_status_change_came_from_akismet( $comments[0]->comment_ID ) ) { Akismet::log( 'Akismet marked it as spam; unspamming.' ); wp_unspam_comment( $comments[0] ); akismet::update_comment_history( $comments[0]->comment_ID, '', 'webhook-ham' ); } else { Akismet::log( 'Comment is not spam, but it has already been manually handled by some other process.' ); Akismet::update_comment_history( $comments[0]->comment_ID, '', 'webhook-ham-noaction' ); } } } $response['comments'][ $guid ] = array( 'status' => 'success' ); } } break; case 'submit-ham': case 'submit-spam': // Nothing to do for submit-ham or submit-spam. break; default: // Unsupported endpoint. break; } /** * Allow plugins to do things with a successfully processed webhook request, like logging. * * @since 5.3.2 * * @param WP_REST_Request $request The REST request object. */ do_action( 'akismet_webhook_received', $request ); Akismet::log( 'Done processing webhook.' ); return rest_ensure_response( $response ); } } How a 40-ounce cup turned Stanley into a $750 million a year business | Modern Business International
spot_img
Saturday, June 14, 2025
HomeNewsHow a 40-ounce cup turned Stanley into a $750 million a year...

How a 40-ounce cup turned Stanley into a $750 million a year business

-


For the better part of the past 110 years, Stanley was doing just fine. 

The drinkware manufacturer had made a place for itself in the knapsacks of outdoorsmen and lunchboxes of blue collar workers with its bottles and thermoses that kept food and drinks hot — or cold — for hours on end. 

The Seattle-based brand was chugging along with a comfortable $70 million in annual sales from its famous hammertone green products, and looked poised for another century of modest, reliable success. 

But starting in 2020, something changed. A fledgling product came into its own and turned Stanley into a juggernaut. 

Over the past four years, the Stanley Quencher has become one of the most popular water bottles in the world. Sold in an ever-growing array of colors and finishes, the Quencher has supercharged Stanley’s sales by appealing to a demographic that Stanley didn’t spend too much time catering to in its first hundred years: women. 

A favorite of nurses, teachers and celebrities alike, the Quencher has been such a popular product that Stanley’s annual sales are projected to top $750 million in 2023, according to data reviewed by CNBC Make It. 

Here’s how Stanley leveraged the Quencher to turn a century-old company into one of the biggest names in hydration.

Early days

The Quencher arrived in 2016 to little fanfare. The 40-ounce insulated cup, which retails for between $45 and $55, sported a handle for ease of transportation, as well as a tapered design that allowed it to slide into a car’s cup holder. 

But in its first few years, the Quencher didn’t make much of an impact. Year after year, the brand’s best-selling product remained the iconic green bottle. Indeed, sales were so middling that by 2019 Stanley had stopped restocking and marketing the product.

In 2020, Stanley brought on Terence Reilly as its new president. Reilly had spent the past seven years at Crocs, where he led the strategy that turned the rubber clogs into one of the hottest shoes on the market. 

When Reilly came onboard, he did a listening tour around the company to hear from employees about what was working and what wasn’t. One employee mentioned a group of women in Utah who ran a commerce blog called The Buy Guide. 

Buy Guide cofounder Ashlee LeSueur had purchased her first Quencher at a Bed, Bath and Beyond store in 2017. She fell in love with the product and quickly began gifting it to friends and recommending it to followers. 

The Quencher retails for between $35 and $55 and comes in dozens of colors and finishes.

Lauren Shamo

In 2019, she tried to make a case for Stanley to continue production of the Quencher, but the sales numbers weren’t there. Instead, Stanley gave her another option: make a wholesale order to sell Quenchers directly to her Buy Guide audience.

“I felt like I was signing a mortgage,” LeSueur tells CNBC Make It of her purchase order for 5,000 Quenchers. “It was a big risk for it. It took every penny that we had in the business account, plus some personal funds to make that happen.”

Those Quenchers, however, sold out within days. When Reilly took charge, he embraced The Buy Guide as partners, working with them to promote new, exciting colors like Desert Sage and Cream. 

“My experience at Crocs told me that that kind of influencer opportunity was just the magic that Stanley might need,” he says. “And we were right. The Buy Guide proved to be amazing partners and helped us create the Quencher phenomenon.”

In fact, the Quencher sold so well that it replaced the iconic Stanley bottle as the brand’s top selling product in 2020. It hasn’t let go of the top spot since. 

Hydration domination 

The success of the Quencher has helped Stanley grow its annual revenue from $70 million to more than $750 million in four years.

Gene Kim

With every new color Stanley rolled out, sales continued to increase. Stanley’s revenue jumped from $73 million in 2019 to $94 million in 2020. It more than doubled to $194 million in 2021.

In 2022, Stanley released a redesigned Quencher model with a streamlined design and new array of colors and finishes. Revenue doubled again that year to $402 million.

The Instagram-friendly pastels helped the Quencher be seen as less of a utilitarian product and more as a fashion accessory. As the available color options grew — Stanley has released the Quencher in over 100 colors — some fans began building collections. 

“We see all the time that [our customer] wants her Quencher to match her fit, her nail polish, her car, her mood, her kitchen,” Reilly tells CNBC Make It. “We’re serving her where she wants the product.” 

Content creator Chelsea Espejo first learned about the Quencher in 2022. She now has a collection of 47 cups. A gym enthusiast, she credits the cup’s large size for helping her stay hydrated throughout her workouts. The wide variety of color options certainly don’t hurt either. 

“On the days that I do have extra time, I search for the specific [color] that matches my shirt,” she tells CNBC Make It. “I wouldn’t even say Stanleys are something I use. They’re actually part of my personality. If I don’t have it, if I don’t choose the right color, my day kind of doesn’t go how I planned it.” 

Espejo isn’t the only one with a strong attachment to her Quencher. The cup  is a social media darling, especially on TikTok. The #StanleyTumbler hashtag has been viewed more than 900 million times, and the product has been the star of many viral videos.

They’re actually part of my personality. If I don’t have [my Stanley], if I don’t choose the right color, my day kind of doesn’t go how I planned it.

Chelsea Espejo

Stanley Quencher collector

Helping drive the excitement is Stanley’s strategy of releasing new colors in limited-edition drops, advertising the latest additions to its roster on social media. Reilly also capitalized on the Quencher’s viral success by pushing for collabs with celebrities and brands. 

“My experience at Crocs was fueled by collaboration culture and drop culture,” Reilly says. “And I knew that once we had our legs under us at Stanley, and once we could see the connection to consumers that we were creating, we were also ready for collaborations.”

Indeed, collabs have been key to driving the Stanley Quencher’s popularity. The Quencher is frequently released in limited edition colors that sell out in minutes. A recent collaboration with Starbucks resulted in a red Quencher that was being resold on eBay for hundreds of dollars the same day it dropped. 

A recent collab with country music star Lainey Wilson sold out in minutes.

Stanley

When Target introduced new Quencher colors recently, some stores had to place restrictions on how many a customer could buy, limiting them to two per person.

“The resale market is certainly flattering,” Reilly says. “The fact that there are signs at America’s best retailers limiting the number of Stanleys you can buy is an astounding thing to think about.”

Quencher collector Emily Fahrlander made her way to her local Starbucks at five in the morning the day the Starbucks collab was released, determined to get her hands on the limited-edition item. 

“I’m not usually that crazy, I really am not,” she tells Make It. “But I didn’t get the last [drop], so I was like, ‘Let’s just wake up early.'” 

And while Reilly and the Stanley team still “want a little bit of scarcity” to help keep excitement around the product, he says they are constantly working on manufacturing as much product as possible.  

“We really continue to increase the number of units available each time we drop, because we see the trend and the waiting lists that are growing,” he says. “But there’s only so many seats in the stadium, and when the seats are sold out, they’re sold out.” 

Halo Effect

Stanley has now sold more than 10 million Quenchers, and demand for the cup doesn’t look to be waning any time soon. 

The Quencher’s popularity on social media has been a boon to the rest of Stanley’s business as well. Ellyn Briggs, a brands analyst at Morning Consult, tells CNBC Make It that a rising tide raises all boats. 

“It’s bringing the Stanley name to the forefront of consumers’ minds, making them aware of the brand, making them have more favorable perceptions,” Briggs tells Make It. 

Indeed, Reilly says that the “entire Stanley brand has benefited from the Quencher trend.” 

Stanley’s entire lineup has adopted the Quencher’s embrace of color.

Lauren Shamo

“We’re seeing our new products checking very well,” he says. “And certainly our heritage products have regained their velocity and their rightful place in culture and in serving the needs of consumers.” 

For Espejo, whose introduction to the brand came through the Quencher, Stanley has become a mainstay in her cupboards with drinking glasses and mugs as well. 

“Now when I go to any store, the first thing I look at is the Stanley collection, whether it’s the mugs or the Quencher,” she says. “My love for [the Quencher] has given me the opportunity to love all of Stanley.”

Close observers might notice that Stanley’s product line has taken a page out of the Quencher’s book, with aesthetic colors and eye-catching designs being adopted by Stanley offerings both new and old.

“[The Quencher redesign] gave us confidence that we can apply those same aesthetic principles across other categories,” Stanley design chief Graham Nearn says. “It gives us confidence that we could even start to refine and define our products that we were most famous for.”

And while the success of the Quencher was fueled in large part by an embrace of colors favored by its new, female audience, Stanley was clearly onto something in its first 110 years. 

One of the Quencher’s most in-demand new colors? Hammertone green. 

DON’T MISS: Want to be smarter and more successful with your money, work & life? Sign up for our new newsletter!

Get CNBC’s free Warren Buffett Guide to Investing, which distills the billionaire’s No. 1 best piece of advice for regular investors, do’s and don’ts, and three key investing principles into a clear and simple guidebook.



Source link a fun game: sprunki horror

Related articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Stay Connected

0FansLike
0FollowersFollow
3,912FollowersFollow
0SubscribersSubscribe
Trump campaign reports raising more than  million after Georgia booking

Latest posts