Call Whop from your server
Use an account API key with member:basic:read, sent as a Bearer token to https://api.whop.com/api/v1. Keep the key on the server. Pin Api-Version-Date so the response shape stays the one you wrote against.
The current API base URL is https://api.whop.com/api/v1. Create the key under the account’s API keys and grant member:basic:read. That is the scope on both List Members and List Memberships. Send it as Authorization: Bearer, the way the API overview shows.
email on a member is null without member:email:read. You do not need email to build a silence list. display_name is on the member, and user.name and user.username are there when the buyer is a person. user is null when the buyer is another business.
Whop versions the API by date. The Members reference documents Api-Version-Date with an example of 2026-10-15-1. Sending that header pins the shapes. Omitting it uses the original 2025-01-01 shapes, which are a different contract. Keep the key out of browser code, mobile apps, and public repos.
List members and read last_accessed_at
GET /members returns one row per buyer. last_accessed_at is when they last opened the account’s content, or null if they never have. It is not a days-since filter, and membership_status=inactive is not that filter either.
GET /members lists one row per buyer, however many memberships they hold. The account id uses a biz_ prefix. account_id defaults to the account the credential acts as. Narrow the rows with access_level=customer and status=joined. customer means they hold a valid product membership. admin is a team member. no_access means every grant has lapsed. joined means they are still part of the account; left means they are not.
last_accessed_at is the ISO 8601 time they last opened the account’s content, or null if they never have. joined_at is when they first joined. You can sort with order=last_accessed_at and direction=asc or desc. That sort does not accept “quiet for 14 days.” created_after and created_before filter join time, not last access. most_recent_action is a lifecycle state such as paid_subscriber or canceling, not a visit.
membership_status=active means the member holds at least one membership that currently grants access. membership_status=inactive means they hold at least one membership that does not. Neither value means “has not opened the product.” Do not use that parameter as the silence filter.
curl --request GET \
--url 'https://api.whop.com/api/v1/members?access_level=customer&status=joined&order=last_accessed_at&direction=asc&first=100' \
--header 'Authorization: Bearer $WHOP_API_KEY' \
--header 'Api-Version-Date: 2026-10-15-1'Follow page_info until the list ends
Each page has a data array and page_info. Pass first up to 100, then pass after with the previous page_info.end_cursor while page_info.has_next_page is true. Stop on a 429 and wait for the delay in the error.
List endpoints return data and page_info. first defaults to 20 and cannot be more than 100. When page_info.has_next_page is true, request the next page with after set to page_info.end_cursor. Stop when has_next_page is false. If the cursor is missing or repeats, stop rather than looping.
Authenticated /api/v1 calls are limited to 600 requests per minute per operation and API credential. Members and memberships are different operations. Over the limit, Whop returns 429 and a message such as a retry delay. Wait for that delay. A 401 is a bad or missing credential. A 403 means the key does not have the scope. Do not retry those by calling again unchanged. Whop’s troubleshooting page states the limit and those statuses.
Bucket whole days into 7, 14, and 30
Count whole days since last_accessed_at. If it is null, count from joined_at and mark them as never opened. Fourteen and 30 days sit inside the 7-day bucket.
Count whole days from today back to last_accessed_at. A gap of 7 or more belongs on the 7-day list, and also on the 14-day or 30-day list when it qualifies. You are timing a check-in, not building three audiences. The same rule is spelled out in find inactive Whop members.
Null means they never opened the account’s content. Count those rows from joined_at and keep them marked as never opened. A timestamp you cannot parse, or one that sits in the future, is not a visit. Leave that member out of the buckets.
Keep access_level of customer and status of joined. Drop admins, members who left, and members whose access has lapsed. Someone who already cancelled is not a win-back candidate on this list.
Read memberships for access and renewal
GET /memberships, with the same member:basic:read scope, tells you which purchases still grant access and when current_period_end falls. Join those rows to members with member_id. On a seller read, the nested member object is null, so do not take last_accessed_at from it.
A member row does not tell you when a plan renews. GET /memberships does. An account API key lists that account’s memberships. account_id narrows the list. member_id is the mber_ id you match to the member. user_id is the buyer, and it is null when the buyer is another business or the membership is unclaimed.
status of active or trialing grants access. past_due is the grace period after a failed payment. completed is a one-time purchase that keeps access. canceled and expired do not. The list filter’s active, trialing, and past_due values exclude memberships set to cancel at period end. Those people can still be quiet and still have access until the period ends. Read cancel_at_period_end on the membership, or request the canceling filter bucket, instead of assuming the active filter is the whole audience.
current_period_end is when the current billing period renews, or when a non-renewing membership expires. It is null for a one-time purchase with no expiration. most_recent_action of renewing_on pairs with most_recent_action_at, which can be a future time. That is the renewal note in the win-back message templates, and only when the date is actually on the membership.
The membership payload includes a nested member object with its own last_accessed_at. Whop’s reference says that object is present only when the membership belongs to the caller, and is null on seller-side reads. Ignore it. The seller’s copy of last_accessed_at is on GET /members. One member can have several memberships. The silence clock stays on the member.
curl --request GET \
--url 'https://api.whop.com/api/v1/memberships?first=100' \
--header 'Authorization: Bearer $WHOP_API_KEY' \
--header 'Api-Version-Date: 2026-10-15-1'Page memberships the same way as members. If a cursor does not move forward, stop and keep the pages you already accepted.
When last-accessed tells you nothing
last_accessed_at describes content opened on the Whop account. It does not describe Discord or Telegram. If Whop is only checkout, this list will not show whether those members are active.
A member can pay on Whop and spend every later day in Discord or Telegram. They will look silent here because they never opened the account’s content after checkout, or because that timestamp never moved. That is not inactivity in the place they use. Do not message the list as if they had gone quiet. If only part of the product is on Whop, treat the timestamp as “they have not opened the Whop side” and check the other side before you write.
Or skip the code
The free inactive-member audit does this read for a connected Whop company: silent counts at 7, 14, and 30 days, and estimated monthly plan value at risk. It does not cancel anyone, change a plan, or send a message. It has the same limit. If members never open Whop, the audit cannot see the app they do use.