getsocialapi-instagram 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,25 @@
1
+ from .client import Client
2
+ from .exceptions import (
3
+ APIKeyError,
4
+ BadRequestError,
5
+ GetSocialAPIError,
6
+ InstagramError,
7
+ NotFoundError,
8
+ RateLimitError,
9
+ ServiceUnavailableError,
10
+ )
11
+ from .instagram import InstagramAPI
12
+
13
+ __version__ = "0.1.0"
14
+
15
+ __all__ = [
16
+ "Client",
17
+ "InstagramAPI",
18
+ "GetSocialAPIError",
19
+ "BadRequestError",
20
+ "APIKeyError",
21
+ "NotFoundError",
22
+ "RateLimitError",
23
+ "ServiceUnavailableError",
24
+ "InstagramError",
25
+ ]
@@ -0,0 +1,31 @@
1
+ import os
2
+
3
+ from .http import DEFAULT_BASE_URL, Transport
4
+ from .instagram import InstagramAPI
5
+
6
+
7
+ class Client:
8
+ """GetSocialAPI client.
9
+
10
+ Usage:
11
+ from getsocialapi_instagram import Client
12
+ client = Client(api_key="gsa_live_...")
13
+ client.instagram.user_by_username("durov")
14
+ """
15
+
16
+ def __init__(self, api_key=None, base_url=DEFAULT_BASE_URL, timeout=30,
17
+ max_retries=3, session=None):
18
+ api_key = api_key or os.environ.get("GETSOCIALAPI_API_KEY")
19
+ if not api_key:
20
+ raise ValueError(
21
+ "api_key is required (pass api_key=... or set GETSOCIALAPI_API_KEY)"
22
+ )
23
+ self._transport = Transport(
24
+ base_url=base_url, api_key=api_key, timeout=timeout,
25
+ max_retries=max_retries, session=session,
26
+ )
27
+ self.instagram = InstagramAPI(self._transport)
28
+
29
+ def health(self):
30
+ """Upstream liveness probe: {"ok": true, "state": "ready"}."""
31
+ return self._transport.request("GET", "/health", auth=False)
@@ -0,0 +1,80 @@
1
+ class GetSocialAPIError(Exception):
2
+ """Base error for all GetSocialAPI SDK failures."""
3
+
4
+ def __init__(self, message, response=None, body=None):
5
+ super().__init__(message)
6
+ self.response = response
7
+ self.body = body if body is not None else {}
8
+
9
+
10
+ def _reason(body, fallback):
11
+ """Extract a human-readable reason: {"error": "..."} | {"error": {"message"}} | {"message": ..."}."""
12
+ err = body.get("error")
13
+ if isinstance(err, dict):
14
+ err = err.get("message")
15
+ if isinstance(err, str) and err:
16
+ return err
17
+ msg = body.get("message")
18
+ if isinstance(msg, str) and msg:
19
+ return msg
20
+ return fallback
21
+
22
+
23
+ class BadRequestError(GetSocialAPIError):
24
+ """400 — invalid parameters (body: {"error", "field"})."""
25
+
26
+ def __init__(self, response=None, body=None):
27
+ body = body or {}
28
+ field = body.get("field")
29
+ msg = _reason(body, "Bad request")
30
+ if field:
31
+ msg = f"{msg} (field: {field})"
32
+ super().__init__(msg, response, body)
33
+ self.field = field
34
+
35
+
36
+ class UnauthorizedError(GetSocialAPIError):
37
+ """401 — invalid or missing API key."""
38
+
39
+ def __init__(self, response=None, body=None):
40
+ super().__init__("Invalid API key", response, body)
41
+
42
+
43
+ class NotFoundError(GetSocialAPIError):
44
+ """404 — target object does not exist. Do not retry."""
45
+
46
+ def __init__(self, response=None, body=None):
47
+ body = body or {}
48
+ msg = _reason(body, "Not found")
49
+ super().__init__(msg, response, body)
50
+
51
+
52
+ class RateLimitError(GetSocialAPIError):
53
+ """429 — key rate limit exceeded. Honors Retry-After."""
54
+
55
+ def __init__(self, response=None, body=None, retry_after=1):
56
+ super().__init__(f"Rate limit exceeded, retry after {retry_after}s", response, body)
57
+ self.retry_after = retry_after
58
+
59
+
60
+ class ServiceUnavailableError(GetSocialAPIError):
61
+ """503 — no live accounts or the service is stopped."""
62
+
63
+ def __init__(self, response=None, body=None, retry_after=None):
64
+ msg = "Service unavailable"
65
+ if retry_after:
66
+ msg += f", retry after {retry_after}s"
67
+ super().__init__(msg, response, body)
68
+ self.retry_after = retry_after
69
+
70
+
71
+ class InstagramError(GetSocialAPIError):
72
+ """Non-404 error returned by the Instagram private API (raw mode)."""
73
+
74
+ def __init__(self, status, message, response=None, body=None):
75
+ super().__init__(f"Instagram API error {status}: {message}", response, body)
76
+ self.status = status
77
+
78
+
79
+ # Convenience alias
80
+ APIKeyError = UnauthorizedError
@@ -0,0 +1,82 @@
1
+ import time
2
+
3
+ import requests
4
+
5
+ from .exceptions import (
6
+ BadRequestError,
7
+ GetSocialAPIError,
8
+ InstagramError,
9
+ NotFoundError,
10
+ RateLimitError,
11
+ ServiceUnavailableError,
12
+ UnauthorizedError,
13
+ )
14
+
15
+ DEFAULT_BASE_URL = "https://api.getsocialapi.com/v1"
16
+
17
+ RETRYABLE_STATUS = {429, 500, 502, 503, 504}
18
+
19
+
20
+ class Transport:
21
+ """HTTP transport: auth, JSON parsing, retries with Retry-After."""
22
+
23
+ def __init__(self, base_url=DEFAULT_BASE_URL, api_key=None, timeout=30,
24
+ max_retries=3, session=None):
25
+ self.base_url = base_url.rstrip("/")
26
+ self.api_key = api_key
27
+ self.timeout = timeout
28
+ self.max_retries = max_retries
29
+ self.session = session or requests.Session()
30
+
31
+ def request(self, method, path, params=None, auth=True):
32
+ url = f"{self.base_url}{path}"
33
+ headers = {"X-API-Key": self.api_key} if auth else {}
34
+ attempts = self.max_retries + 1
35
+ last_exc = None
36
+ for attempt in range(attempts):
37
+ try:
38
+ resp = self.session.request(
39
+ method, url, params=params, headers=headers, timeout=self.timeout,
40
+ )
41
+ except (requests.ConnectionError, requests.Timeout) as e:
42
+ last_exc = e
43
+ if attempt < attempts - 1:
44
+ time.sleep(min(2 ** attempt, 10))
45
+ continue
46
+ raise GetSocialAPIError(f"Network error: {e}") from e
47
+
48
+ if resp.status_code in RETRYABLE_STATUS and attempt < attempts - 1:
49
+ time.sleep(self._retry_after(resp))
50
+ continue
51
+ return self._parse(resp)
52
+ raise GetSocialAPIError("Request failed after retries") # pragma: no cover
53
+
54
+ @staticmethod
55
+ def _retry_after(resp):
56
+ try:
57
+ return min(max(int(float(resp.headers.get("Retry-After", 1))), 0), 30)
58
+ except (TypeError, ValueError):
59
+ return 1
60
+
61
+ def _parse(self, resp):
62
+ try:
63
+ body = resp.json()
64
+ except ValueError:
65
+ body = {"raw": resp.text}
66
+
67
+ if resp.status_code == 401:
68
+ raise UnauthorizedError(resp, body)
69
+ if resp.status_code == 400:
70
+ raise BadRequestError(resp, body)
71
+ if resp.status_code == 404:
72
+ raise NotFoundError(resp, body)
73
+ if resp.status_code == 429:
74
+ raise RateLimitError(resp, body, retry_after=self._retry_after(resp))
75
+ if resp.status_code == 503:
76
+ raise ServiceUnavailableError(resp, body, retry_after=self._retry_after(resp))
77
+ if resp.status_code >= 400:
78
+ # Instagram errors in raw mode: {"message": "...", "status": "fail"}
79
+ if isinstance(body, dict) and body.get("status") == "fail":
80
+ raise InstagramError(resp.status_code, body.get("message", "unknown"), resp, body)
81
+ raise GetSocialAPIError(f"HTTP {resp.status_code}: {body}", resp, body)
82
+ return body
@@ -0,0 +1,257 @@
1
+ from urllib.parse import quote
2
+ import re
3
+
4
+
5
+ def _quote(value):
6
+ return quote(str(value), safe="")
7
+
8
+
9
+ def _media_arg(value, name="media id"):
10
+ """Numeric media pk or shortcode (e.g. DehVsgBi3sH)."""
11
+ s = str(value)
12
+ if s.isdigit():
13
+ return _digits(s, name)
14
+ if re.fullmatch(r"[A-Za-z0-9_-]{5,64}", s):
15
+ return s
16
+ raise ValueError(f"{name} must be a numeric pk or a shortcode, "
17
+ f"got: {value!r}")
18
+
19
+
20
+ def _digits(value, name, max_len=20):
21
+ s = str(value)
22
+ if not s.isdigit() or not 1 <= len(s) <= max_len:
23
+ raise ValueError(f"{name} must be 1-{max_len} digits, got: {value!r}")
24
+ return s
25
+
26
+
27
+ def _length(value, name, lo, hi):
28
+ s = str(value)
29
+ if not lo <= len(s) <= hi:
30
+ raise ValueError(f"{name} must be {lo}-{hi} characters, got: {value!r}")
31
+ return s
32
+
33
+
34
+ def _tag(value):
35
+ return _quote(_length(str(value).lstrip("#"), "tag", 1, 64))
36
+
37
+
38
+ def _count(value, lo=1, hi=50, default=None):
39
+ if value is None:
40
+ return default
41
+ if isinstance(value, bool) or not isinstance(value, int) or not lo <= value <= hi:
42
+ raise ValueError(f"count must be an integer {lo}-{hi}, got: {value!r}")
43
+ return value
44
+
45
+
46
+ class InstagramAPI:
47
+ """Instagram endpoints of GetSocialAPI.
48
+
49
+ Methods map 1:1 to API routes and return the raw JSON responses as received.
50
+ """
51
+
52
+ def __init__(self, transport):
53
+ self._t = transport
54
+
55
+ # ---------- Profile ----------
56
+
57
+ def user_info(self, user_id):
58
+ """GET /ig/user/info/{user_id} — profile by numeric id."""
59
+ return self._t.request("GET", f"/ig/user/info/{_digits(user_id, 'user_id')}")
60
+
61
+ def user_by_username(self, username):
62
+ """GET /ig/user/username_info/{username} — profile by username."""
63
+ return self._t.request("GET", f"/ig/user/username_info/{_length(username, 'username', 1, 64)}")
64
+
65
+ def profile(self, user_id):
66
+ """GET /profile/{user_id} — profile by numeric id."""
67
+ return self._t.request("GET", f"/profile/{_digits(user_id, 'user_id')}")
68
+
69
+ def user_about(self, user_id):
70
+ """GET /user/about/{user_id} — About this account.
71
+
72
+ The "About this account" screen: country, registration month,
73
+ former usernames count, accounts-with-shared-followers count,
74
+ verification. Sparse accounts return null fields — check
75
+ `has_about_data` / `unavailable` before using them.
76
+ """
77
+ return self._t.request("GET", f"/user/about/{_digits(user_id, 'user_id')}")
78
+
79
+ def search_username(self, query, count=10):
80
+ """GET /search/username/{query} — search users.
81
+
82
+ `count` is forwarded to the service, but the response may include
83
+ one extra exact-match entry beyond it.
84
+ """
85
+ return self._t.request("GET", f"/search/username/{_quote(query)}",
86
+ params={"count": _count(count, default=10)})
87
+
88
+ def similar_users(self, user_id):
89
+ """GET /ig/user/similar/{user_id} — suggested accounts."""
90
+ return self._t.request("GET", f"/ig/user/similar/{_digits(user_id, 'user_id')}")
91
+
92
+ # ---------- User content ----------
93
+
94
+ def user_medias(self, user_id, count=12, max_id=None):
95
+ """GET /ig/user/media/{user_id} — profile grid posts."""
96
+ return self._t.request("GET", f"/ig/user/media/{_digits(user_id, 'user_id')}",
97
+ params=_page(count, max_id, default=12))
98
+
99
+ def user_tags(self, user_id, count=12, max_id=None):
100
+ """GET /ig/user/tags/{user_id} — posts where the user is tagged."""
101
+ return self._t.request("GET", f"/ig/user/tags/{_digits(user_id, 'user_id')}",
102
+ params=_page(count, max_id, default=12))
103
+
104
+ def user_stories(self, user_id):
105
+ """GET /ig/user/stories/{user_id} — active stories."""
106
+ return self._t.request("GET", f"/ig/user/stories/{_digits(user_id, 'user_id')}")
107
+
108
+ def user_highlights(self, user_id):
109
+ """GET /ig/user/highlights/{user_id} — highlights tray."""
110
+ return self._t.request("GET", f"/ig/user/highlights/{_digits(user_id, 'user_id')}")
111
+
112
+ def highlight_stories(self, highlight_id):
113
+ """GET /ig/highlight/stories/{highlight_id} — media of a highlight.
114
+
115
+ Accepts the tray id as is — the leading `highlight:` prefix
116
+ (as returned by `user_highlights`) is trimmed automatically.
117
+ """
118
+ hid = str(highlight_id)
119
+ if hid.startswith("highlight:"):
120
+ hid = hid[len("highlight:"):]
121
+ return self._t.request("GET", f"/ig/highlight/stories/{_quote(_length(hid, 'highlight_id', 1, 64))}")
122
+
123
+ # ---------- Followers / following ----------
124
+
125
+ def followers(self, user_id, count=50, max_id=None, query=None):
126
+ """GET /followers/{user_id} — followers list.
127
+
128
+ On unverified accounts `count`, the `next_max_id` cursor and the
129
+ `query` filter work — nearly the full list is accessible and
130
+ searchable. On verified accounts (blue checkmark) Instagram exposes
131
+ no more than ~50 followers: one page, no cursor, `query` ignored.
132
+ """
133
+ params = _page(count, max_id, default=50)
134
+ if query is not None:
135
+ params["query"] = query
136
+ return self._t.request("GET", f"/followers/{_digits(user_id, 'user_id')}",
137
+ params=params)
138
+
139
+ def following(self, user_id, count=25, max_id=None, query=None):
140
+ """GET /following/{user_id} — following list.
141
+
142
+ Page size: the service returns the default 25 accounts per page and
143
+ at most 25 (values above 50 are rejected). The response carries
144
+ `next_max_id` — pass it as `max_id` for the next page. `query`
145
+ filters the list by name.
146
+ """
147
+ params = _page(count, max_id, default=25, hi=50)
148
+ if query is not None:
149
+ params["query"] = query
150
+ return self._t.request("GET", f"/following/{_digits(user_id, 'user_id')}",
151
+ params=params)
152
+
153
+ # ---------- Media ----------
154
+
155
+ def media_info(self, media):
156
+ """GET /ig/media/info/{pk|shortcode} — full media card.
157
+
158
+ Accepts a numeric pk or a post shortcode (e.g. "Deu3BoCBbDP"):
159
+ items[0] carries pk, code, media_type (1=photo, 2=video,
160
+ 8=carousel), caption, like/comment/view counts, taken_at, owner.
161
+ """
162
+ return self._t.request("GET", f"/ig/media/info/{_media_arg(media)}")
163
+
164
+ def media_likers(self, media):
165
+ """GET /ig/media/likers/{pk|shortcode} — users who liked the media.
166
+
167
+ One shot of up to ~100 users; the route has no pagination.
168
+ """
169
+ return self._t.request("GET", f"/ig/media/likers/{_media_arg(media)}")
170
+
171
+ def media_comments(self, media, count=None, sort_order="recent", min_id=None):
172
+ """GET /ig/media/comments/{pk|shortcode} — media comments.
173
+
174
+ Accepts a numeric pk or a post shortcode (e.g. "Deu3BoCBbDP").
175
+
176
+ Full pagination works only with `sort_order="recent"` (default):
177
+ pages are strictly chronological (newer → older) and disjoint.
178
+ The cursor is `next_min_id` from the previous response — pass it
179
+ back as `min_id` verbatim (it is a JSON string with special
180
+ characters; the transport URL-encodes it). Collect until
181
+ `next_min_id` is null/empty or `comments` is an empty list.
182
+
183
+ Instagram never returns `next_max_id` for this route — `max_id`
184
+ pagination must not be used. `count` (1-50) is accepted by the
185
+ validator but the real page size is ~12-20 comments. Without
186
+ `sort_order` (ranked) the service returns a ~15 comment preview
187
+ whose pages overlap between pool accounts.
188
+ """
189
+ params = {}
190
+ if count is not None:
191
+ params["count"] = _count(count)
192
+ if sort_order is not None:
193
+ if sort_order not in ("recent", "ranked"):
194
+ raise ValueError('sort_order must be "recent" or "ranked", '
195
+ f"got: {sort_order!r}")
196
+ params["sort_order"] = sort_order
197
+ if min_id is not None:
198
+ params["min_id"] = min_id
199
+ return self._t.request("GET", f"/ig/media/comments/{_media_arg(media)}",
200
+ params=params)
201
+
202
+ def search_clips(self, keyword):
203
+ """GET /ig/media/search_clips/{keyword} — search reels by keyword.
204
+
205
+ The service returns its native page (~12 clips); a page-size
206
+ parameter is not supported by the API for this route.
207
+ """
208
+ kw = _length(keyword, "keyword", 1, 100)
209
+ return self._t.request("GET", f"/ig/media/search_clips/{_quote(kw)}")
210
+
211
+ # ---------- Hashtags ----------
212
+
213
+ def hashtag_info(self, tag):
214
+ """GET /ig/hashtag/info/{tag} — hashtag card. A leading # is trimmed."""
215
+ return self._t.request("GET", f"/ig/hashtag/info/{_tag(tag)}")
216
+
217
+ def hashtag_search(self, query):
218
+ """GET /ig/hashtag/search/{query} — search hashtags.
219
+
220
+ Returns the full first page of tag matches; a page-size
221
+ parameter is not supported by the API for this route.
222
+ """
223
+ return self._t.request("GET", f"/ig/hashtag/search/{_quote(query)}")
224
+
225
+ def hashtag_posts(self, tag, max_id=None):
226
+ """GET /v2/hashtag/posts/{tag} — posts by hashtag. A leading # is trimmed.
227
+
228
+ The service returns its native page (~15-18 posts) plus `next_max_id`
229
+ for pagination via `max_id`; a page-size parameter is not supported.
230
+ """
231
+ params = {"max_id": max_id} if max_id is not None else None
232
+ return self._t.request("GET", f"/v2/hashtag/posts/{_tag(tag)}",
233
+ params=params)
234
+
235
+ # ---------- Locations ----------
236
+
237
+ def location_info(self, pk):
238
+ """GET /ig/location/info/{pk} — location card."""
239
+ return self._t.request("GET", f"/ig/location/info/{_digits(pk, 'pk')}")
240
+
241
+ def location_media(self, pk, max_id=None):
242
+ """GET /ig/location/media/{pk} — media by location.
243
+
244
+ The service returns its native page; pass `next_max_id` from a
245
+ previous response as `max_id` to request the next page. The
246
+ response also carries `next_page` — a page counter, not a cursor.
247
+ """
248
+ params = {"max_id": max_id} if max_id is not None else None
249
+ return self._t.request("GET", f"/ig/location/media/{_digits(pk, 'pk')}",
250
+ params=params)
251
+
252
+
253
+ def _page(count, max_id, default=12, hi=50):
254
+ params = {"count": _count(count, default=default, hi=hi)}
255
+ if max_id is not None:
256
+ params["max_id"] = max_id
257
+ return params
@@ -0,0 +1,184 @@
1
+ Metadata-Version: 2.4
2
+ Name: getsocialapi-instagram
3
+ Version: 0.1.0
4
+ Summary: GetSocialAPI — client library for the Instagram API (no cookies, no accounts required)
5
+ Author: GetSocialAPI
6
+ License: MIT
7
+ Project-URL: Homepage, https://getsocialapi.com
8
+ Project-URL: Documentation, https://getsocialapi.com/docs/
9
+ Keywords: instagram,api,scraping,sdk,getsocialapi
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
+ Requires-Python: >=3.9
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: requests>=2.25
19
+ Dynamic: license-file
20
+
21
+ # Instagram Scraper API
22
+
23
+ [![tests](https://github.com/GetSocialAPI/Instagram-Scraper-API/actions/workflows/tests.yml/badge.svg)](https://github.com/GetSocialAPI/Instagram-Scraper-API/actions/workflows/tests.yml)
24
+ [![license](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
25
+ [![python](https://img.shields.io/badge/python-3.9%2B-blue)](https://www.python.org)
26
+
27
+ Python client for the **[GetSocialAPI](https://getsocialapi.com/)** Instagram Scraper API.
28
+
29
+ - **No cookies required** — you never provide Instagram session cookies
30
+ - **No accounts needed** — zero ban risk for your own profiles
31
+ - Raw Instagram JSON responses, REST endpoints — no transformations
32
+ - Automatic retries with `Retry-After`, typed exceptions
33
+
34
+ ## Installation
35
+
36
+ ```bash
37
+ pip install getsocialapi-instagram
38
+ ```
39
+
40
+ ## Migration from `getsocialapi` (git installs)
41
+
42
+ Before the PyPI release this SDK was installed from git as `getsocialapi`.
43
+ Old imports keep working via a deprecated shim, but please migrate:
44
+
45
+ ```bash
46
+ pip uninstall getsocialapi
47
+ pip install getsocialapi-instagram
48
+ ```
49
+
50
+ ```python
51
+ # before
52
+ from getsocialapi import Client
53
+ # after
54
+ from getsocialapi_instagram import Client
55
+ ```
56
+
57
+ ## Quickstart
58
+
59
+ ```python
60
+ from getsocialapi_instagram import Client
61
+
62
+ # Get your API key at https://getsocialapi.com/dashboard/
63
+ client = Client(api_key="gsa_live_...") # or set GETSOCIALAPI_API_KEY
64
+
65
+ # Profile by numeric id (or user_by_username for usernames)
66
+ user = client.instagram.user_info(25025320)
67
+ print(user["user"]["follower_count"])
68
+
69
+ # Posts (grid)
70
+ medias = client.instagram.user_medias(25025320, count=12)
71
+
72
+ # Followers / following — paginated on unverified accounts: pass `next_max_id`
73
+ # back as `max_id`. Verified accounts (blue checkmark) expose ~50 followers max.
74
+ page = client.instagram.followers(43586162288, count=25)
75
+ page = client.instagram.followers(43586162288, max_id=page["next_max_id"])
76
+
77
+ # All comments of a post — cursor is next_min_id -> min_id (sort_order="recent" by default)
78
+ comments, cursor = [], None
79
+ while True:
80
+ page = client.instagram.media_comments(4012386322294878415, count=50, min_id=cursor)
81
+ comments += page["comments"]
82
+ cursor = page.get("next_min_id")
83
+ if not cursor:
84
+ break
85
+ ```
86
+
87
+ Base URL: `https://api.getsocialapi.com/v1` — authenticate with the API key
88
+ from your [dashboard](https://getsocialapi.com/dashboard/).
89
+
90
+ ## Client
91
+
92
+ ```python
93
+ Client(api_key=None, base_url="https://api.getsocialapi.com/v1", timeout=30, max_retries=3)
94
+ ```
95
+
96
+ | Parameter | Default | Description |
97
+ | --- | --- | --- |
98
+ | `api_key` | env `GETSOCIALAPI_API_KEY` | your API key |
99
+ | `base_url` | API endpoint | override only for testing |
100
+ | `timeout` | `30` | per-request timeout, seconds |
101
+ | `max_retries` | `3` | retries on 429/503/5xx and network errors |
102
+
103
+ ## Methods
104
+
105
+ | Method | Route | Description |
106
+ | --- | --- | --- |
107
+ | `profile(user_id)` | `/profile/<id>` | profile by numeric id |
108
+ | `user_info(user_id)` | `/ig/user/info/<id>` | profile by numeric id |
109
+ | `user_by_username(username)` | `/ig/user/username_info/<username>` | profile by username |
110
+ | `user_about(user_id)` | `/user/about/<id>` | About this account: country, joined, former usernames, shared-followers count, verified |
111
+ | `search_username(query, count=10)` | `/search/username/<q>` | search users, resolve username → pk |
112
+ | `similar_users(user_id)` | `/ig/user/similar/<id>` | suggested accounts |
113
+ | `user_medias(user_id, count=12, max_id=None)` | `/ig/user/media/<id>` | profile grid posts |
114
+ | `user_tags(user_id, count=12, max_id=None)` | `/ig/user/tags/<id>` | posts with the user tagged |
115
+ | `user_stories(user_id)` | `/ig/user/stories/<id>` | active stories |
116
+ | `user_highlights(user_id)` | `/ig/user/highlights/<id>` | highlights tray |
117
+ | `highlight_stories(highlight_id)` | `/ig/highlight/stories/<id>` | media of a highlight |
118
+ | `followers(user_id, count=50, max_id=None, query=None)` | `/followers/<id>` | followers list |
119
+ | `following(user_id, count=25, max_id=None, query=None)` | `/following/<id>` | following list |
120
+ | `media_info(media)` | `/ig/media/info/<pk\|shortcode>` | full media card; accepts numeric pk or shortcode |
121
+ | `media_likers(media)` | `/ig/media/likers/<pk\|shortcode>` | users who liked (up to ~100, no pagination) |
122
+ | `media_comments(media, count=None, sort_order="recent", min_id=None)` | `/ig/media/comments/<pk\|shortcode>` | media comments; cursor pagination `next_min_id` → `min_id` |
123
+ | `search_clips(keyword)` | `/ig/media/search_clips/<kw>` | search reels by keyword |
124
+ | `hashtag_info(tag)` | `/ig/hashtag/info/<tag>` | hashtag card |
125
+ | `hashtag_search(query)` | `/ig/hashtag/search/<q>` | search hashtags |
126
+ | `hashtag_posts(tag, max_id=None)` | `/v2/hashtag/posts/<tag>` | posts by hashtag |
127
+ | `location_info(pk)` | `/ig/location/info/<pk>` | location card |
128
+ | `location_media(pk, max_id=None)` | `/ig/location/media/<pk>` | media by location |
129
+
130
+ Every method returns the raw JSON of the API route as received — nothing is
131
+ unwrapped, filtered or renamed. Where the API supports pagination (`followers`*, `following`, `user_medias`,
132
+ `user_tags`, `hashtag_posts`, `location_media`), pass the cursor from the
133
+ previous response (`next_max_id`) as `max_id`.
134
+ `media_comments` paginates with a different cursor: always request with
135
+ `sort_order="recent"` (the default) and pass `next_min_id` back as `min_id`
136
+ until it comes back null — pages are chronological and disjoint. Instagram
137
+ never returns `next_max_id` for comments, so `max_id` must not be used there.
138
+ *`followers` works on unverified accounts (nearly the full list, plus the
139
+ `query` filter); verified accounts (blue checkmark) expose no more than ~50
140
+ followers — one page, no cursor, `query` ignored. For lists without cursors
141
+ (`search_clips`, `hashtag_search`) each call simply returns the service page.
142
+
143
+ Full request/response reference with live examples:
144
+ [docs.getsocialapi → API Reference](https://getsocialapi.com/docs/api/instagram/user/get_info_by_id/)
145
+ (see the Documentation section on [getsocialapi.com](https://getsocialapi.com/)).
146
+
147
+ ## Errors
148
+
149
+ | Exception | HTTP | Notes |
150
+ | --- | --- | --- |
151
+ | `GetSocialAPIError` | — | base class; also network errors |
152
+ | `APIKeyError` | 401 | invalid or missing key |
153
+ | `BadRequestError` | 400 | `.field` points to the bad parameter |
154
+ | `NotFoundError` | 404 | object does not exist — never retried |
155
+ | `RateLimitError` | 429 | `.retry_after` seconds |
156
+ | `ServiceUnavailableError` | 503 | no live accounts, `.retry_after` |
157
+ | `InstagramError` | any | upstream Instagram error, `.status` |
158
+
159
+ ```python
160
+ from getsocialapi_instagram import NotFoundError, RateLimitError
161
+ import time
162
+
163
+ try:
164
+ client.instagram.user_info(25025320)
165
+ except NotFoundError:
166
+ ... # object does not exist
167
+ except RateLimitError as e:
168
+ time.sleep(e.retry_after)
169
+ ```
170
+
171
+ Only successful responses (HTTP `200` and `404`) count against your quota.
172
+ Rate limits and plans: [pricing](https://getsocialapi.com/#pricing).
173
+
174
+ ## Support
175
+
176
+ - Telegram: [@getsocialapi](https://t.me/getsocialapi)
177
+ - Email: [hello@getsocialapi.com](mailto:hello@getsocialapi.com)
178
+ - Website: [getsocialapi.com](https://getsocialapi.com/)
179
+ - Documentation: [getsocialapi.com/docs](https://getsocialapi.com/docs/)
180
+ - Status: [getsocialapi.com/status](https://getsocialapi.com/status/)
181
+
182
+ ## License
183
+
184
+ MIT
@@ -0,0 +1,10 @@
1
+ getsocialapi_instagram/__init__.py,sha256=rVO7FwwDBbG1JwrSCmmSzJApGqRv3unhiW6KOTtvrH4,475
2
+ getsocialapi_instagram/client.py,sha256=UWySIdI0KaADaa2B_kSXCRSqo_tgN61KIC8ruTIzctU,1035
3
+ getsocialapi_instagram/exceptions.py,sha256=-pC5E7stZ0fdlVHmE4ro7TCyPObvg8usA-AoJvoUmNQ,2567
4
+ getsocialapi_instagram/http.py,sha256=tUnyMhVIdMxG56pD9QZutrpD1-0DkCXlFOTTwgtck-M,2961
5
+ getsocialapi_instagram/instagram.py,sha256=obR1WE2s1Aa_9C3G5cUoN5TJmXZoWp6epIf5581QK2M,10601
6
+ getsocialapi_instagram-0.1.0.dist-info/licenses/LICENSE,sha256=jgv_wwS6fmja7z85ssgPuXIDkB6DTDr-v03_ClRjkyY,1069
7
+ getsocialapi_instagram-0.1.0.dist-info/METADATA,sha256=kAqfy98Ov8ixniUqIMiOdXzfYaOZy_by4qTGR-GYDNE,8011
8
+ getsocialapi_instagram-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
9
+ getsocialapi_instagram-0.1.0.dist-info/top_level.txt,sha256=UWWVCJiWXLbG-OqIGGPFPcZ5zqFU193d9VMb_CFGu50,23
10
+ getsocialapi_instagram-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 GetSocialAPI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ getsocialapi_instagram