Skip to content

Commit 1a8acd1

Browse files
feat(api): api update
1 parent 5f775d7 commit 1a8acd1

32 files changed

Lines changed: 659 additions & 310 deletions

‎.stats.yml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
configured_endpoints: 265
2-
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/onlyfansapi/onlyfansapi-e4abd4c83686ea74647398a46d6991a121861e309ae311ea7a362c4cc9666a34.yml
3-
openapi_spec_hash: cc4be956948ae5b19f83ad3b6fdb9e8d
2+
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/onlyfansapi/onlyfansapi-34e6f479a1872e539a6ba6bbd44e66e1a10d978ae34a53a7f7499c4e93bafd2e.yml
3+
openapi_spec_hash: 17e3d768656db29acf3d2068449bfa52
44
config_hash: 397c91e15c0024f8b5bbed9b82c2348c

‎src/onlyfansapi/resources/chats/messages.py‎

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import httpx
99

1010
from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given
11-
from ..._utils import path_template, maybe_transform, async_maybe_transform
11+
from ..._utils import path_template, maybe_transform, strip_not_given, async_maybe_transform
1212
from ..._compat import cached_property
1313
from ..._resource import SyncAPIResource, AsyncAPIResource
1414
from ..._response import (
@@ -367,6 +367,7 @@ def send(
367367
rf_partner: str | Omit = omit,
368368
rf_tag: str | Omit = omit,
369369
text: str | Omit = omit,
370+
idempotency_key: str | Omit = omit,
370371
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
371372
# The extra values given here take precedence over values defined on the client or passed to this method.
372373
extra_headers: Headers | None = None,
@@ -377,6 +378,30 @@ def send(
377378
"""
378379
Send a new message to a chat.
379380
381+
**Idempotency.** Pass an `Idempotency-Key` header to make retries safe. The
382+
first request with a given key is executed normally and its response is stored
383+
for **24 hours**; any later request with the same key returns that stored
384+
response, plus an `Idempotent-Replayed: true` header, without contacting
385+
OnlyFans and without consuming credits. The replayed body is the original
386+
response with its `_meta._credits` block rewritten to show `used: 0` and your
387+
current balance.
388+
389+
Keys are scoped to your team, this endpoint and the account in the URL, so the
390+
same value can be reused safely against a different account. Use a fresh, unique
391+
value (a UUID works well) for each message you send; it must be 1-255 printable
392+
ASCII characters.
393+
394+
- `400 IDEMPOTENCY_KEY_INVALID` — the header value is empty, too long, or
395+
contains non-ASCII characters.
396+
- `409 IDEMPOTENCY_CONFLICT` — an earlier request with this key is still
397+
running. Retry once it finishes.
398+
- `422 IDEMPOTENCY_KEY_MISMATCH` — this key was already used with a different
399+
request body or chat.
400+
401+
Responses with a `5xx` status (and `408`/`429`) are never stored, so a failed
402+
send can be retried with the same key. The header is optional: omit it and the
403+
endpoint behaves exactly as before.
404+
380405
Args:
381406
block_banned_words: Screen `text` for OnlyFans banned words and block the send if any are found
382407
(returns a 422 listing the offending words). `strict_ban` blocks all tiers,
@@ -421,6 +446,7 @@ def send(
421446
raise ValueError(f"Expected a non-empty value for `account` but received {account!r}")
422447
if not chat_id:
423448
raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}")
449+
extra_headers = {**strip_not_given({"Idempotency-Key": idempotency_key}), **(extra_headers or {})}
424450
return self._post(
425451
path_template("/api/{account}/chats/{chat_id}/messages", account=account, chat_id=chat_id),
426452
body=maybe_transform(
@@ -869,6 +895,7 @@ async def send(
869895
rf_partner: str | Omit = omit,
870896
rf_tag: str | Omit = omit,
871897
text: str | Omit = omit,
898+
idempotency_key: str | Omit = omit,
872899
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
873900
# The extra values given here take precedence over values defined on the client or passed to this method.
874901
extra_headers: Headers | None = None,
@@ -879,6 +906,30 @@ async def send(
879906
"""
880907
Send a new message to a chat.
881908
909+
**Idempotency.** Pass an `Idempotency-Key` header to make retries safe. The
910+
first request with a given key is executed normally and its response is stored
911+
for **24 hours**; any later request with the same key returns that stored
912+
response, plus an `Idempotent-Replayed: true` header, without contacting
913+
OnlyFans and without consuming credits. The replayed body is the original
914+
response with its `_meta._credits` block rewritten to show `used: 0` and your
915+
current balance.
916+
917+
Keys are scoped to your team, this endpoint and the account in the URL, so the
918+
same value can be reused safely against a different account. Use a fresh, unique
919+
value (a UUID works well) for each message you send; it must be 1-255 printable
920+
ASCII characters.
921+
922+
- `400 IDEMPOTENCY_KEY_INVALID` — the header value is empty, too long, or
923+
contains non-ASCII characters.
924+
- `409 IDEMPOTENCY_CONFLICT` — an earlier request with this key is still
925+
running. Retry once it finishes.
926+
- `422 IDEMPOTENCY_KEY_MISMATCH` — this key was already used with a different
927+
request body or chat.
928+
929+
Responses with a `5xx` status (and `408`/`429`) are never stored, so a failed
930+
send can be retried with the same key. The header is optional: omit it and the
931+
endpoint behaves exactly as before.
932+
882933
Args:
883934
block_banned_words: Screen `text` for OnlyFans banned words and block the send if any are found
884935
(returns a 422 listing the offending words). `strict_ban` blocks all tiers,
@@ -923,6 +974,7 @@ async def send(
923974
raise ValueError(f"Expected a non-empty value for `account` but received {account!r}")
924975
if not chat_id:
925976
raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}")
977+
extra_headers = {**strip_not_given({"Idempotency-Key": idempotency_key}), **(extra_headers or {})}
926978
return await self._post(
927979
path_template("/api/{account}/chats/{chat_id}/messages", account=account, chat_id=chat_id),
928980
body=await async_maybe_transform(

‎src/onlyfansapi/resources/fans/fans.py‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,12 @@ def list_active(
149149
track progress, GET `/{account}/me` returns data.subscribersCount (the current
150150
active-subscriber count) as a total.
151151
152+
Supports `filter[max_total_spent]` (e.g. `filter[max_total_spent]=0` for fans
153+
who have never spent), which OnlyFans itself cannot do. Those requests are
154+
answered from OnlyFansAPI's own fan index rather than proxied, so the page is
155+
limited to fans we have already indexed for this account — see `data._source` in
156+
the response.
157+
152158
Args:
153159
limit: Number of fans to return (1-20). OnlyFans does not allow more than 20 per page.
154160
Must be at least 1. Must not be greater than 20.
@@ -214,6 +220,12 @@ def list_all(
214220
OnlyFans occasionally returns fewer than `limit` items (e.g. 19 for limit=20) on
215221
a non-final page because it filters entries server-side; no fans are skipped.
216222
223+
Supports `filter[max_total_spent]` (e.g. `filter[max_total_spent]=0` for fans
224+
who have never spent), which OnlyFans itself cannot do. Those requests are
225+
answered from OnlyFansAPI's own fan index rather than proxied, so the page is
226+
limited to fans we have already indexed for this account — see `data._source` in
227+
the response.
228+
217229
Args:
218230
limit: Number of fans to return (1-20). OnlyFans does not allow more than 20 per page.
219231
Must be at least 1. Must not be greater than 20.
@@ -280,6 +292,12 @@ def list_expired(
280292
limit=20) on a non-final page because it filters entries server-side; no fans
281293
are skipped.
282294
295+
Supports `filter[max_total_spent]` (e.g. `filter[max_total_spent]=0` for fans
296+
who have never spent), which OnlyFans itself cannot do. Those requests are
297+
answered from OnlyFansAPI's own fan index rather than proxied, so the page is
298+
limited to fans we have already indexed for this account — see `data._source` in
299+
the response.
300+
283301
Args:
284302
limit: Number of fans to return (1-20). OnlyFans does not allow more than 20 per page.
285303
Must be at least 1. Must not be greater than 20.
@@ -581,6 +599,12 @@ async def list_active(
581599
track progress, GET `/{account}/me` returns data.subscribersCount (the current
582600
active-subscriber count) as a total.
583601
602+
Supports `filter[max_total_spent]` (e.g. `filter[max_total_spent]=0` for fans
603+
who have never spent), which OnlyFans itself cannot do. Those requests are
604+
answered from OnlyFansAPI's own fan index rather than proxied, so the page is
605+
limited to fans we have already indexed for this account — see `data._source` in
606+
the response.
607+
584608
Args:
585609
limit: Number of fans to return (1-20). OnlyFans does not allow more than 20 per page.
586610
Must be at least 1. Must not be greater than 20.
@@ -646,6 +670,12 @@ async def list_all(
646670
OnlyFans occasionally returns fewer than `limit` items (e.g. 19 for limit=20) on
647671
a non-final page because it filters entries server-side; no fans are skipped.
648672
673+
Supports `filter[max_total_spent]` (e.g. `filter[max_total_spent]=0` for fans
674+
who have never spent), which OnlyFans itself cannot do. Those requests are
675+
answered from OnlyFansAPI's own fan index rather than proxied, so the page is
676+
limited to fans we have already indexed for this account — see `data._source` in
677+
the response.
678+
649679
Args:
650680
limit: Number of fans to return (1-20). OnlyFans does not allow more than 20 per page.
651681
Must be at least 1. Must not be greater than 20.
@@ -712,6 +742,12 @@ async def list_expired(
712742
limit=20) on a non-final page because it filters entries server-side; no fans
713743
are skipped.
714744
745+
Supports `filter[max_total_spent]` (e.g. `filter[max_total_spent]=0` for fans
746+
who have never spent), which OnlyFans itself cannot do. Those requests are
747+
answered from OnlyFansAPI's own fan index rather than proxied, so the page is
748+
limited to fans we have already indexed for this account — see `data._source` in
749+
the response.
750+
715751
Args:
716752
limit: Number of fans to return (1-20). OnlyFans does not allow more than 20 per page.
717753
Must be at least 1. Must not be greater than 20.

‎src/onlyfansapi/resources/media/media.py‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,9 @@ def upload(
200200
201201
Args:
202202
async_: Set to `true` to process uploads in the background. Returns a `polling_url` to
203-
check status. Recommended for large files.
203+
check status. Recommended for large files. Instead of polling, you can subscribe
204+
to the `media_uploads.completed` and `media_uploads.failed` webhook events —
205+
they only fire for async uploads.
204206
205207
file:
206208
The file to upload. Required if `file_url` is not provided. Maximum file size:
@@ -405,7 +407,9 @@ async def upload(
405407
406408
Args:
407409
async_: Set to `true` to process uploads in the background. Returns a `polling_url` to
408-
check status. Recommended for large files.
410+
check status. Recommended for large files. Instead of polling, you can subscribe
411+
to the `media_uploads.completed` and `media_uploads.failed` webhook events —
412+
they only fire for async uploads.
409413
410414
file:
411415
The file to upload. Required if `file_url` is not provided. Maximum file size:

‎src/onlyfansapi/resources/media/uploads.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ def get_status(
6666
- `completed` — Upload finished, `media` and `credits_used` are included
6767
- `failed` — Upload failed, `error` is included
6868
69+
Instead of polling, you can subscribe to the `media_uploads.completed` and
70+
`media_uploads.failed` webhook events. They carry the same fields as this
71+
endpoint and are only sent for async (`async=true`) uploads — synchronous
72+
uploads return their result directly.
73+
6974
Args:
7075
extra_headers: Send extra headers
7176
@@ -137,6 +142,11 @@ async def get_status(
137142
- `completed` — Upload finished, `media` and `credits_used` are included
138143
- `failed` — Upload failed, `error` is included
139144
145+
Instead of polling, you can subscribe to the `media_uploads.completed` and
146+
`media_uploads.failed` webhook events. They carry the same fields as this
147+
endpoint and are only sent for async (`async=true`) uploads — synchronous
148+
uploads return their result directly.
149+
140150
Args:
141151
extra_headers: Send extra headers
142152

‎src/onlyfansapi/resources/media/vault/vault.py‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,9 @@ def upload(
232232
233233
Args:
234234
async_: Set to `true` to process uploads in the background. Returns a `polling_url` to
235-
check status. Recommended for large files.
235+
check status. Recommended for large files. Instead of polling, you can subscribe
236+
to the `media_uploads.completed` and `media_uploads.failed` webhook events —
237+
they only fire for async uploads.
236238
237239
file:
238240
The file to upload. Required if `file_url` is not provided. Maximum file size:
@@ -461,7 +463,9 @@ async def upload(
461463
462464
Args:
463465
async_: Set to `true` to process uploads in the background. Returns a `polling_url` to
464-
check status. Recommended for large files.
466+
check status. Recommended for large files. Instead of polling, you can subscribe
467+
to the `media_uploads.completed` and `media_uploads.failed` webhook events —
468+
they only fire for async uploads.
465469
466470
file:
467471
The file to upload. Required if `file_url` is not provided. Maximum file size:

‎src/onlyfansapi/resources/user_lists/users.py‎

Lines changed: 54 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
from __future__ import annotations
44

5+
from typing import Any, cast
6+
57
import httpx
68

79
from ..._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given
@@ -104,6 +106,7 @@ def add(
104106
*,
105107
account: str,
106108
ids: SequenceNotStr[str],
109+
skip_invalid: bool | Omit = omit,
107110
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
108111
# The extra values given here take precedence over values defined on the client or passed to this method.
109112
extra_headers: Headers | None = None,
@@ -117,6 +120,14 @@ def add(
117120
Args:
118121
ids: Array of OnlyFans User IDs to be added into the list
119122
123+
skip_invalid: Set to `true` to skip the User IDs OnlyFans refuses instead of failing the whole
124+
batch. We drop the rejected IDs and retry the remainder for you (up to 5
125+
OnlyFans attempts, each costing 1 credit), then respond `200` with `data.added`
126+
(the IDs that made it in) and `data.failed` (an object mapping each rejected
127+
User ID to the reason OnlyFans gave). Note this changes the shape of `data` —
128+
see the example responses. Failures that are not about individual users (e.g. an
129+
invalid or inaccessible list ID) still return the regular `400`.
130+
120131
extra_headers: Send extra headers
121132
122133
extra_query: Add additional query parameters to the request
@@ -129,13 +140,24 @@ def add(
129140
raise ValueError(f"Expected a non-empty value for `account` but received {account!r}")
130141
if not user_list_id:
131142
raise ValueError(f"Expected a non-empty value for `user_list_id` but received {user_list_id!r}")
132-
return self._post(
133-
path_template("/api/{account}/user-lists/{user_list_id}/users", account=account, user_list_id=user_list_id),
134-
body=maybe_transform({"ids": ids}, user_add_params.UserAddParams),
135-
options=make_request_options(
136-
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
143+
return cast(
144+
UserAddResponse,
145+
self._post(
146+
path_template(
147+
"/api/{account}/user-lists/{user_list_id}/users", account=account, user_list_id=user_list_id
148+
),
149+
body=maybe_transform(
150+
{
151+
"ids": ids,
152+
"skip_invalid": skip_invalid,
153+
},
154+
user_add_params.UserAddParams,
155+
),
156+
options=make_request_options(
157+
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
158+
),
159+
cast_to=cast(Any, UserAddResponse), # Union types cannot be passed in as arguments in the type system
137160
),
138-
cast_to=UserAddResponse,
139161
)
140162

141163
def clear(
@@ -391,6 +413,7 @@ async def add(
391413
*,
392414
account: str,
393415
ids: SequenceNotStr[str],
416+
skip_invalid: bool | Omit = omit,
394417
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
395418
# The extra values given here take precedence over values defined on the client or passed to this method.
396419
extra_headers: Headers | None = None,
@@ -404,6 +427,14 @@ async def add(
404427
Args:
405428
ids: Array of OnlyFans User IDs to be added into the list
406429
430+
skip_invalid: Set to `true` to skip the User IDs OnlyFans refuses instead of failing the whole
431+
batch. We drop the rejected IDs and retry the remainder for you (up to 5
432+
OnlyFans attempts, each costing 1 credit), then respond `200` with `data.added`
433+
(the IDs that made it in) and `data.failed` (an object mapping each rejected
434+
User ID to the reason OnlyFans gave). Note this changes the shape of `data` —
435+
see the example responses. Failures that are not about individual users (e.g. an
436+
invalid or inaccessible list ID) still return the regular `400`.
437+
407438
extra_headers: Send extra headers
408439
409440
extra_query: Add additional query parameters to the request
@@ -416,13 +447,24 @@ async def add(
416447
raise ValueError(f"Expected a non-empty value for `account` but received {account!r}")
417448
if not user_list_id:
418449
raise ValueError(f"Expected a non-empty value for `user_list_id` but received {user_list_id!r}")
419-
return await self._post(
420-
path_template("/api/{account}/user-lists/{user_list_id}/users", account=account, user_list_id=user_list_id),
421-
body=await async_maybe_transform({"ids": ids}, user_add_params.UserAddParams),
422-
options=make_request_options(
423-
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
450+
return cast(
451+
UserAddResponse,
452+
await self._post(
453+
path_template(
454+
"/api/{account}/user-lists/{user_list_id}/users", account=account, user_list_id=user_list_id
455+
),
456+
body=await async_maybe_transform(
457+
{
458+
"ids": ids,
459+
"skip_invalid": skip_invalid,
460+
},
461+
user_add_params.UserAddParams,
462+
),
463+
options=make_request_options(
464+
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
465+
),
466+
cast_to=cast(Any, UserAddResponse), # Union types cannot be passed in as arguments in the type system
424467
),
425-
cast_to=UserAddResponse,
426468
)
427469

428470
async def clear(

‎src/onlyfansapi/types/chats/message_send_params.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,5 @@ class MessageSendParams(TypedDict, total=False):
6868

6969
text: str
7070
"""The message text content. Required unless a media file is present."""
71+
72+
idempotency_key: Annotated[str, PropertyInfo(alias="Idempotency-Key")]

0 commit comments

Comments
 (0)