Substack API endpoint
Returns one public Substack profile: identity, the publications the writer contributes to, their publicly visible subscriptions, and every subscriber signal Substack publishes. total is opt-in per writer and rounded to roughly three significant figures; when a writer hides it, total_hidden is true and total is 0, which must not be read as zero subscribers. paid_rough, paid_rough_int, and paid_tier are buckets - no exact paid subscriber count is published anywhere. follower_count is exact but counts followers, not subscribers.
/substack/userhandle: required Substack profile handle, for example
pragmaticengineer. Numeric user ids are not accepted by the upstream and
are rejected.Substack publishes four different subscriber signals at three different precisions. Only one is close to exact.
| Field | Precision | Always present |
|---|---|---|
total / total_display | Total (free + paid), ~3 significant figures | No — opt-in per writer |
total_hidden | Flag: writer hid the total | Yes |
paid_rough / paid_rough_int | Paid subscribers, bucketed | Yes |
paid_tier | Paid subscribers, coarser badge threshold (100, 1000, 10000) | Yes |
follower_count | Exact | Yes |
Three cautions:
total_hidden: true means the writer hid the count, not that they have
no subscribers. total is 0 in that case. Fall back to paid_rough.paid_rough describes paid subscribers despite reading like a total.
A writer with 1,100,000 total subscribers reports
paid_rough: "Tens of thousands"; the two fields measure different
things and are not inconsistent.follower_count is not a subscriber count. It is exact, but it counts
profile followers, which is typically far larger than either subscriber
figure.subscriptions lists only the subscriptions the user chose to make public.
membership_state values observed on this surface include free_signup;
is_founding marks a founding-tier membership.
| Field | Filled in, of 38 checked | Values in the sample |
|---|---|---|
| publication_id | 38 | |
| publication_name | 38 | |
| subdomain | 38 | |
| membership_state | 38 | |
| is_founding | 38 | true in 0 of 38 |
| Parameter | Type | Required | Default | Description | Example |
|---|---|---|---|---|---|
| handle | string | Yes | Substack profile handle | pragmaticengineer | |
| x-api-key (header) | string | Yes | API key required |
curl -X GET "https://api.crawlora.net/api/v1/substack/user?handle=pragmaticengineer" \ -H "x-api-key: $CRAWLORA_API_KEY"
{
"code": 200,
"data": {
"id": 30107029,
"handle": "pragmaticengineer",
"name": "Gergely Orosz",
"url": "https://substack.com/@pragmaticengineer",
"bestseller_tier": 10000,
"subscribers": {
"total": 1100000,
"total_display": "1.1M+ subscribers",
"total_hidden": false,
"paid_rough": "Tens of thousands",
"paid_rough_int": 10000,
"paid_tier": 10000,
"follower_count": 1210186
},
"publications": [
{
"id": 458709,
"name": "The Pragmatic Engineer",
"subdomain": "pragmaticengineer",
"url": "https://pragmaticengineer.substack.com",
"role": "admin",
"is_primary": true
}
],
"subscriptions": [
{
"publication_id": 355849,
"publication_name": "Dispatches From the Basement",
"membership_state": "free_signup",
"is_founding": false
}
]
},
"msg": "success"
}Request schema
No body schema
Response schema
#/definitions/substack.userResponseDoc
| Field | Type | Required | Enum | Bounds | Example | Description |
|---|---|---|---|---|---|---|
| code | integer | No | 200 | |||
| data | substack.User | No | ||||
| data.bestseller_tier | integer | No | 10000 | |||
| data.bio | string | No | ||||
| data.handle | string | No | pragmaticengineer | |||
| data.id | integer | No | 2067309 | |||
| data.name | string | No | Gergely Orosz | |||
| data.photo_url | string | No | ||||
| data.profile_set_up_at | string | No | ||||
| data.publications | array | No | ||||
| data.publications[].custom_domain | string | No | ||||
| data.publications[].id | integer | No | 458709 | |||
| data.publications[].is_primary | boolean | No | ||||
| data.publications[].name | string | No | The Pragmatic Engineer | |||
| data.publications[].role | string | No | admin | |||
| data.publications[].subdomain | string | No | ||||
| data.publications[].url | string | No | ||||
| data.slug | string | No | ||||
| data.subscribers | substack.SubscriberCounts | No | ||||
| data.subscribers.follower_count | integer | No | 365727 | |||
| data.subscribers.paid_rough | string | No | Thousands of paid subscribers | PaidRough and PaidRoughInt are the only paid-subscriber figures Substack publishes, and both are buckets rather than counts. PaidRoughInt is the finer of the two and is always present; PaidTier is the coarser threshold behind the public bestseller badge. | ||
| data.subscribers.paid_rough_int | integer | No | 1000 | |||
| data.subscribers.paid_tier | integer | No | 1000 | |||
| data.subscribers.total | integer | No | 315000 | |||
| data.subscribers.total_display | string | No | 315K+ | |||
| data.subscribers.total_hidden | boolean | No | ||||
| data.subscribers.total_rough | string | No | Hundreds of thousands of subscribers | |||
| data.subscriptions | array | No | ||||
| data.subscriptions[].is_founding | boolean | No | ||||
| data.subscriptions[].membership_state | string | No | free_signup | |||
| data.subscriptions[].publication_id | integer | No | ||||
| data.subscriptions[].publication_name | string | No | ||||
| data.subscriptions[].subdomain | string | No | ||||
| data.url | string | No | https://substack.com/@pragmaticengineer | |||
| msg | string | No | success |
| Status | Description | Schema |
|---|---|---|
| 400 | Bad Request | #/definitions/app.Response |
| 404 | Not Found | #/definitions/app.Response |
| 429 | Too Many Requests | #/definitions/app.Response |
| 503 | Service Unavailable | #/definitions/app.Response |
Use environment variables for secrets and keep Crawlora API keys server-side.
curl -X GET "https://api.crawlora.net/api/v1/substack/user?handle=pragmaticengineer" \
-H "x-api-key: $CRAWLORA_API_KEY"